From bad8dc0f7328f5c923b95ec9e822a4960be143d5 Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Thu, 24 Jul 2025 19:19:53 +0000 Subject: [PATCH] more progress in agi and support external tools --- hesabixCore/config/services.yaml | 26 + .../src/AiTool/AccountingDocService.php | 37 ++ hesabixCore/src/AiTool/CommodityService.php | 37 ++ hesabixCore/src/AiTool/PersonService.php | 39 ++ hesabixCore/src/Cog/AccountingDocService.php | 116 ++++ hesabixCore/src/Cog/CommodityService.php | 107 ++++ hesabixCore/src/Cog/PersonService.php | 212 ++++++- .../src/Controller/AdminController.php | 3 + .../src/Controller/CommodityController.php | 135 +---- .../src/Controller/HesabdariController.php | 96 +--- .../src/Controller/PersonsController.php | 284 +--------- .../src/Controller/wizardController.php | 197 ++++++- hesabixCore/src/Service/AGI/AGIService.php | 165 ++++-- .../AGI/Promps/AccountingDocPromptService.php | 93 ++++ .../Service/AGI/Promps/BasePromptService.php | 140 +---- .../AGI/Promps/InventoryPromptService.php | 138 ++--- .../AGI/Promps/PersonPromptService.php | 199 ++++++- .../src/Service/AGI/Promps/PromptService.php | 17 +- .../views/user/manager/settings/system.vue | 18 +- webUI/src/views/wizard/home.vue | 523 +++++++++++++++++- 20 files changed, 1837 insertions(+), 745 deletions(-) create mode 100644 hesabixCore/src/AiTool/AccountingDocService.php create mode 100644 hesabixCore/src/AiTool/CommodityService.php create mode 100644 hesabixCore/src/Cog/AccountingDocService.php create mode 100644 hesabixCore/src/Cog/CommodityService.php create mode 100644 hesabixCore/src/Service/AGI/Promps/AccountingDocPromptService.php diff --git a/hesabixCore/config/services.yaml b/hesabixCore/config/services.yaml index af0b248..9272c0a 100644 --- a/hesabixCore/config/services.yaml +++ b/hesabixCore/config/services.yaml @@ -97,6 +97,32 @@ services: tags: ['twig.extension'] App\Cog\PersonService: + arguments: + $entityManager: '@doctrine.orm.entity_manager' + + App\Service\AGI\Promps\AccountingDocPromptService: + arguments: + $entityManager: '@doctrine.orm.entity_manager' + + App\Service\AGI\Promps\BasePromptService: arguments: $entityManager: '@doctrine.orm.entity_manager' $access: '@App\Service\Access' + + App\Service\AGI\Promps\PromptService: + arguments: + $entityManager: '@doctrine.orm.entity_manager' + $personPromptService: '@App\Service\AGI\Promps\PersonPromptService' + $basePromptService: '@App\Service\AGI\Promps\BasePromptService' + $inventoryPromptService: '@App\Service\AGI\Promps\InventoryPromptService' + $bankPromptService: '@App\Service\AGI\Promps\BankPromptService' + $accountingDocPromptService: '@App\Service\AGI\Promps\AccountingDocPromptService' + + App\Cog\AccountingDocService: + arguments: + $entityManager: '@doctrine.orm.entity_manager' + + App\AiTool\AccountingDocService: + arguments: + $em: '@doctrine.orm.entity_manager' + $cogAccountingDocService: '@App\Cog\AccountingDocService' diff --git a/hesabixCore/src/AiTool/AccountingDocService.php b/hesabixCore/src/AiTool/AccountingDocService.php new file mode 100644 index 0000000..e82a7db --- /dev/null +++ b/hesabixCore/src/AiTool/AccountingDocService.php @@ -0,0 +1,37 @@ +em = $em; + $this->cogAccountingDocService = $cogAccountingDocService; + } + + /** + * جست‌وجوی ردیف‌های اسناد حسابداری برای ابزار هوش مصنوعی + */ + public function searchRowsAi(array $params, $acc = null): array + { + $acc = $acc ?? ($params['acc'] ?? null); + if (!$acc) { + return [ + 'error' => 'اطلاعات دسترسی (acc) الزامی است' + ]; + } + try { + return $this->cogAccountingDocService->searchRows($params, $acc); + } catch (\Exception $e) { + return [ + 'error' => 'خطا در جست‌وجوی ردیف‌های اسناد: ' . $e->getMessage() + ]; + } + } +} \ No newline at end of file diff --git a/hesabixCore/src/AiTool/CommodityService.php b/hesabixCore/src/AiTool/CommodityService.php new file mode 100644 index 0000000..f1692c6 --- /dev/null +++ b/hesabixCore/src/AiTool/CommodityService.php @@ -0,0 +1,37 @@ +em = $em; + $this->cogCommodityService = $cogCommodityService; + } + + /** + * افزودن یا ویرایش کالا برای ابزار هوش مصنوعی + */ + public function addOrUpdateCommodityAi(array $params, $acc = null, $code = 0): array + { + $acc = $acc ?? ($params['acc'] ?? null); + if (!$acc) { + return [ + 'error' => 'اطلاعات دسترسی (acc) الزامی است' + ]; + } + try { + return $this->cogCommodityService->addOrUpdateCommodity($params, $acc, $code ?? ($params['code'] ?? 0)); + } catch (\Exception $e) { + return [ + 'error' => 'خطا در افزودن/ویرایش کالا: ' . $e->getMessage() + ]; + } + } +} \ No newline at end of file diff --git a/hesabixCore/src/AiTool/PersonService.php b/hesabixCore/src/AiTool/PersonService.php index 3d2ddc7..7f63416 100644 --- a/hesabixCore/src/AiTool/PersonService.php +++ b/hesabixCore/src/AiTool/PersonService.php @@ -32,6 +32,7 @@ class PersonService ]; } try { + // فقط کد را به سرویس Cog پاس بده return $this->cogPersonService->getPersonInfo($code, $acc); } catch (\Exception $e) { return [ @@ -40,5 +41,43 @@ class PersonService } } + /** + * دریافت لیست اشخاص با فیلتر و صفحه‌بندی برای ابزار هوش مصنوعی + */ + public function getPersonsListAi(array $params, $acc = null): array + { + $acc = $acc ?? ($params['acc'] ?? null); + if (!$acc) { + return [ + 'error' => 'اطلاعات دسترسی (acc) الزامی است' + ]; + } + try { + return $this->cogPersonService->getPersonsList($params, $acc); + } catch (\Exception $e) { + return [ + 'error' => 'خطا در دریافت لیست اشخاص: ' . $e->getMessage() + ]; + } + } + /** + * افزودن یا ویرایش شخص برای ابزار هوش مصنوعی + */ + public function addOrUpdatePersonAi(array $params, $acc = null, $code = 0): array + { + $acc = $acc ?? ($params['acc'] ?? null); + if (!$acc) { + return [ + 'error' => 'اطلاعات دسترسی (acc) الزامی است' + ]; + } + try { + return $this->cogPersonService->addOrUpdatePerson($params, $acc, $code ?? ($params['code'] ?? 0)); + } catch (\Exception $e) { + return [ + 'error' => 'خطا در افزودن/ویرایش شخص: ' . $e->getMessage() + ]; + } + } } \ No newline at end of file diff --git a/hesabixCore/src/Cog/AccountingDocService.php b/hesabixCore/src/Cog/AccountingDocService.php new file mode 100644 index 0000000..0fd0d54 --- /dev/null +++ b/hesabixCore/src/Cog/AccountingDocService.php @@ -0,0 +1,116 @@ +entityManager = $entityManager; + } + + /** + * جست‌وجوی ردیف‌های اسناد حسابداری بر اساس نوع و شناسه + * @param array $params + * @param array $acc + * @return array + */ + public function searchRows(array $params, array $acc): array + { + $em = $this->entityManager; + $data = []; + if (!isset($params['type'])) { + return ['error' => 'نوع (type) الزامی است']; + } + $roll = ''; + if ($params['type'] == 'person') + $roll = 'person'; + if ($params['type'] == 'person_receive' || $params['type'] == 'person_send') + $roll = 'person'; + elseif ($params['type'] == 'sell_receive') + $roll = 'sell'; + elseif ($params['type'] == 'bank') + $roll = 'banks'; + elseif ($params['type'] == 'buy_send') + $roll = 'buy'; + elseif ($params['type'] == 'transfer') + $roll = 'bankTransfer'; + elseif ($params['type'] == 'all') + $roll = 'accounting'; + else + $roll = $params['type']; + // اینجا فرض می‌کنیم acc معتبر است و قبلاً بررسی شده + if ($params['type'] == 'person') { + $person = $em->getRepository(\App\Entity\Person::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $params['id'], + ]); + if (!$person) + return ['error' => 'شخص یافت نشد']; + $data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([ + 'person' => $person, + ], [ + 'id' => 'DESC' + ]); + } elseif ($params['type'] == 'bank') { + $bank = $em->getRepository(\App\Entity\BankAccount::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $params['id'], + ]); + if (!$bank) + return ['error' => 'بانک یافت نشد']; + $data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([ + 'bank' => $bank, + ], [ + 'id' => 'DESC' + ]); + } elseif ($params['type'] == 'cashdesk') { + $cashdesk = $em->getRepository(\App\Entity\Cashdesk::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $params['id'], + ]); + if (!$cashdesk) + return ['error' => 'صندوق یافت نشد']; + $data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([ + 'cashdesk' => $cashdesk, + ], [ + 'id' => 'DESC' + ]); + } elseif ($params['type'] == 'salary') { + $salary = $em->getRepository(\App\Entity\Salary::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $params['id'], + ]); + if (!$salary) + return ['error' => 'حقوق یافت نشد']; + $data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([ + 'salary' => $salary, + ], [ + 'id' => 'DESC' + ]); + } else { + return ['error' => 'نوع پشتیبانی نمی‌شود']; + } + $dataTemp = []; + foreach ($data as $item) { + $temp = [ + 'id' => $item->getId(), + 'dateSubmit' => $item->getDoc()->getDateSubmit(), + 'date' => $item->getDoc()->getDate(), + 'type' => $item->getDoc()->getType(), + 'ref' => $item->getRef()->getName(), + 'des' => $item->getDes(), + 'bs' => $item->getBs(), + 'bd' => $item->getBd(), + 'code' => $item->getDoc()->getCode(), + 'submitter' => $item->getDoc()->getSubmitter()->getFullName() + ]; + $dataTemp[] = $temp; + } + return $dataTemp; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Cog/CommodityService.php b/hesabixCore/src/Cog/CommodityService.php new file mode 100644 index 0000000..5ddf79a --- /dev/null +++ b/hesabixCore/src/Cog/CommodityService.php @@ -0,0 +1,107 @@ +entityManager = $entityManager; + } + + /** + * افزودن یا ویرایش کالا/خدمات + * @param array $params + * @param array $acc + * @param int|string $code + * @return array + */ + public function addOrUpdateCommodity(array $params, array $acc, $code = 0): array + { + $em = $this->entityManager; + if (!isset($params['name']) || trim($params['name']) === '') + return ['result' => -1, 'error' => 'نام کالا الزامی است']; + if ($code == 0) { + $data = $em->getRepository(Commodity::class)->findOneBy([ + 'name' => $params['name'], + 'bid' => $acc['bid'] + ]); + if (!$data) { + $data = new Commodity(); + $data->setCode((new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'Commodity')); + } + } else { + $data = $em->getRepository(Commodity::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code + ]); + if (!$data) + return ['result' => -2, 'error' => 'کالا یافت نشد']; + } + $unit = null; + if (!isset($params['unit'])) + $unit = $em->getRepository(CommodityUnit::class)->findAll()[0]; + else + $unit = $em->getRepository(CommodityUnit::class)->findOneBy(['name' => $params['unit']]); + if (!$unit) + return ['result' => -3, 'error' => 'واحد کالا یافت نشد']; + $data->setUnit($unit); + $data->setBid($acc['bid']); + $data->setName($params['name']); + $data->setKhadamat($params['khadamat'] ?? false); + $data->setWithoutTax($params['withoutTax'] ?? false); + if (isset($params['des'])) $data->setDes($params['des']); + if (isset($params['priceSell'])) $data->setPriceSell($params['priceSell']); + if (isset($params['priceBuy'])) $data->setPriceBuy($params['priceBuy']); + if (isset($params['commodityCountCheck'])) $data->setCommodityCountCheck($params['commodityCountCheck']); + if (isset($params['barcodes'])) $data->setBarcodes($params['barcodes']); + if (isset($params['taxCode'])) $data->setTaxCode($params['taxCode']); + if (isset($params['taxType'])) $data->setTaxType($params['taxType']); + if (isset($params['taxUnit'])) $data->setTaxUnit($params['taxUnit']); + if (isset($params['minOrderCount'])) $data->setMinOrderCount($params['minOrderCount']); + if (isset($params['speedAccess'])) $data->setSpeedAccess($params['speedAccess']); + if (isset($params['dayLoading'])) $data->setDayLoading($params['dayLoading']); + if (isset($params['orderPoint'])) $data->setOrderPoint($params['orderPoint']); + // دسته‌بندی + if (isset($params['cat']) && $params['cat'] != '') { + $cat = is_array($params['cat']) ? $em->getRepository(CommodityCat::class)->find($params['cat']['id']) : $em->getRepository(CommodityCat::class)->find($params['cat']); + if ($cat && $cat->getBid() == $acc['bid']) { + $data->setCat($cat); + } + } + $em->persist($data); + // قیمت‌ها + if (isset($params['prices'])) { + foreach ($params['prices'] as $item) { + $priceList = $em->getRepository(PriceList::class)->findOneBy([ + 'bid' => $acc['bid'], + 'id' => $item['list']['id'] + ]); + if ($priceList) { + $detail = $em->getRepository(PriceListDetail::class)->findOneBy([ + 'list' => $priceList, + 'commodity' => $data + ]); + if (!$detail) $detail = new PriceListDetail(); + $detail->setList($priceList); + $detail->setCommodity($data); + $detail->setPriceSell($item['priceSell']); + $detail->setPriceBuy(0); + $detail->setMoney($acc['money']); + $em->persist($detail); + } + } + } + $em->flush(); + return ['Success' => true, 'result' => 1, 'code' => $data->getId()]; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Cog/PersonService.php b/hesabixCore/src/Cog/PersonService.php index a8d8e86..01f33cd 100644 --- a/hesabixCore/src/Cog/PersonService.php +++ b/hesabixCore/src/Cog/PersonService.php @@ -16,15 +16,13 @@ use App\Service\Explore; class PersonService { private EntityManagerInterface $entityManager; - private array $access; /** * سازنده سرویس */ - public function __construct(EntityManagerInterface $entityManager, array $access) + public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; - $this->access = $access; } /** @@ -74,4 +72,212 @@ class PersonService return $response; } + + /** + * دریافت لیست اشخاص با فیلتر، جست‌وجو و صفحه‌بندی + * + * @param array $params پارامترهای جست‌وجو و فیلتر + * @param array $acc اطلاعات دسترسی + * @return array + */ + public function getPersonsList(array $params, array $acc): array + { + $page = $params['page'] ?? 1; + $itemsPerPage = $params['itemsPerPage'] ?? 10; + $search = $params['search'] ?? ''; + $types = $params['types'] ?? null; + $transactionFilters = $params['transactionFilters'] ?? null; + + $queryBuilder = $this->entityManager->getRepository(Person::class) + ->createQueryBuilder('p') + ->where('p.bid = :bid') + ->setParameter('bid', $acc['bid']); + + if (!empty($search) || $search === '0') { + $search = trim($search); + $queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search') + ->setParameter('search', "%$search%"); + } + + if ($types && !empty($types)) { + $queryBuilder->leftJoin('p.type', 't') + ->andWhere('t.code IN (:types)') + ->setParameter('types', $types); + } + + $totalItems = (clone $queryBuilder) + ->select('COUNT(p.id)') + ->getQuery() + ->getSingleScalarResult(); + + $persons = $queryBuilder + ->select('p') + ->setFirstResult(($page - 1) * $itemsPerPage) + ->setMaxResults($itemsPerPage) + ->getQuery() + ->getResult(); + + $response = []; + foreach ($persons as $person) { + $rows = $this->entityManager->getRepository(HesabdariRow::class)->findBy([ + 'person' => $person, + 'bid' => $acc['bid'] + ]); + $bs = 0; + $bd = 0; + foreach ($rows as $row) { + $doc = $row->getDoc(); + if ($doc && $doc->getMoney() && $doc->getYear() && + $doc->getMoney()->getId() == $acc['money']->getId() && + $doc->getYear()->getId() == $acc['year']->getId()) { + $bs += (float) $row->getBs(); + $bd += (float) $row->getBd(); + } + } + $balance = $bs - $bd; + + $include = true; + if ($transactionFilters && !empty($transactionFilters)) { + $include = false; + if (in_array('debtors', $transactionFilters) && $balance < 0) { + $include = true; + } + if (in_array('creditors', $transactionFilters) && $balance > 0) { + $include = true; + } + if (in_array('zero', $transactionFilters) && $balance == 0) { + $include = true; + } + } + + if ($include) { + $result = Explore::ExplorePerson($person, $this->entityManager->getRepository(PersonType::class)->findAll()); + $result['bs'] = $bs; + $result['bd'] = $bd; + $result['balance'] = $balance; + $response[] = $result; + } + } + + $filteredTotal = count($response); + + return [ + 'items' => array_slice($response, 0, $itemsPerPage), + 'total' => $filteredTotal, + 'unfilteredTotal' => $totalItems, + ]; + } + + /** + * افزودن یا ویرایش شخص + * @param array $params + * @param array $acc + * @param int|string $code + * @return array + */ + public function addOrUpdatePerson(array $params, array $acc, $code = 0): array + { + $em = $this->entityManager; + if (!isset($params['nikename']) || trim($params['nikename']) === '') + return ['result' => -1, 'error' => 'نام مستعار الزامی است']; + + if ($code == 0) { + $person = $em->getRepository(\App\Entity\Person::class)->findOneBy([ + 'nikename' => $params['nikename'], + 'bid' => $acc['bid'] + ]); + if (!$person) { + $person = new \App\Entity\Person(); + $maxAttempts = 10; + $newCode = null; + for ($i = 0; $i < $maxAttempts; $i++) { + $newCode = $params['code'] ?? $code; + if (!$newCode || $newCode == 0) { + $newCode = (new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'person'); + } + $exist = $em->getRepository(\App\Entity\Person::class)->findOneBy(['code' => $newCode]); + if (!$exist) break; + } + if ($newCode === null) return ['result' => -2, 'error' => 'کد جدید تولید نشد']; + $person->setCode($newCode); + } + } else { + $person = $em->getRepository(\App\Entity\Person::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code + ]); + if (!$person) return ['result' => -3, 'error' => 'شخص یافت نشد']; + } + $person->setBid($acc['bid']); + $person->setNikename($params['nikename']); + if (isset($params['name'])) $person->setName($params['name']); + if (isset($params['birthday'])) $person->setBirthday($params['birthday']); + if (isset($params['tel'])) $person->setTel($params['tel']); + if (isset($params['speedAccess'])) $person->setSpeedAccess($params['speedAccess']); + if (isset($params['address'])) $person->setAddress($params['address']); + if (isset($params['des'])) $person->setDes($params['des']); + if (isset($params['mobile'])) $person->setMobile($params['mobile']); + if (isset($params['mobile2'])) $person->setMobile2($params['mobile2']); + if (isset($params['fax'])) $person->setFax($params['fax']); + if (isset($params['website'])) $person->setWebsite($params['website']); + if (isset($params['email'])) $person->setEmail($params['email']); + if (isset($params['postalcode'])) $person->setPostalcode($params['postalcode']); + if (isset($params['shahr'])) $person->setShahr($params['shahr']); + if (isset($params['ostan'])) $person->setOstan($params['ostan']); + if (isset($params['keshvar'])) $person->setKeshvar($params['keshvar']); + if (isset($params['sabt'])) $person->setSabt($params['sabt']); + if (isset($params['codeeghtesadi'])) $person->setCodeeghtesadi($params['codeeghtesadi']); + if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']); + if (isset($params['company'])) $person->setCompany($params['company']); + if (array_key_exists('prelabel', $params)) { + if ($params['prelabel'] != '') { + $prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]); + if ($prelabel) $person->setPrelabel($prelabel); + } elseif ($params['prelabel'] == null) { + $person->setPrelabel(null); + } + } + // کارت‌ها + if (isset($params['accounts'])) { + foreach ($params['accounts'] as $item) { + $card = $em->getRepository(\App\Entity\PersonCard::class)->findOneBy([ + 'bid' => $acc['bid'], + 'person' => $person, + 'bank' => $item['bank'] + ]); + if (!$card) $card = new \App\Entity\PersonCard(); + $card->setPerson($person); + $card->setBid($acc['bid']); + $card->setShabaNum($item['shabaNum']); + $card->setCardNum($item['cardNum']); + $card->setAccountNum($item['accountNum']); + $card->setBank($item['bank']); + $em->persist($card); + } + // حذف کارت‌های حذف‌شده + $accounts = $em->getRepository(\App\Entity\PersonCard::class)->findBy([ + 'bid' => $acc['bid'], + 'person' => $person, + ]); + foreach ($accounts as $item) { + $deleted = true; + foreach ($params['accounts'] as $param) { + if ($item->getBank() == $param['bank']) $deleted = false; + } + if ($deleted) $em->remove($item); + } + } + // نوع‌ها + if (isset($params['types'])) { + $types = $em->getRepository(\App\Entity\PersonType::class)->findAll(); + foreach ($params['types'] as $item) { + $typeEntity = $em->getRepository(\App\Entity\PersonType::class)->findOneBy(['code' => $item['code']]); + if ($item['checked'] == true) $person->addType($typeEntity); + elseif ($item['checked'] == false) $person->removeType($typeEntity); + } + } + $em->persist($person); + $em->flush(); + return ['Success' => true, 'result' => 1]; + } } \ No newline at end of file diff --git a/hesabixCore/src/Controller/AdminController.php b/hesabixCore/src/Controller/AdminController.php index ce6121d..bba72a1 100644 --- a/hesabixCore/src/Controller/AdminController.php +++ b/hesabixCore/src/Controller/AdminController.php @@ -469,6 +469,7 @@ class AdminController extends AbstractController $resp['inputTokenPrice'] = $registryMGR->get('system', key: 'inputTokenPrice'); $resp['outputTokenPrice'] = $registryMGR->get('system', key: 'outputTokenPrice'); $resp['aiPrompt'] = $registryMGR->get('system', key: 'aiPrompt'); + $resp['aiDebugMode'] = $registryMGR->get('system', key: 'aiDebugMode'); return $this->json($resp); } @@ -521,6 +522,8 @@ class AdminController extends AbstractController $registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice'] ?? ''); if (array_key_exists('aiPrompt', $params)) $registryMGR->update('system', 'aiPrompt', $params['aiPrompt'] ?? ''); + if (array_key_exists('aiDebugMode', $params)) + $registryMGR->update('system', 'aiDebugMode', $params['aiDebugMode'] ?? ''); $entityManager->persist($item); $entityManager->flush(); diff --git a/hesabixCore/src/Controller/CommodityController.php b/hesabixCore/src/Controller/CommodityController.php index 3415fdf..aa60d79 100644 --- a/hesabixCore/src/Controller/CommodityController.php +++ b/hesabixCore/src/Controller/CommodityController.php @@ -823,138 +823,13 @@ class CommodityController extends AbstractController if ($content = $request->getContent()) { $params = json_decode($content, true); } - if (!array_key_exists('name', $params)) - return $this->json(['result' => -1]); - if (count_chars(trim($params['name'])) == 0) - return $this->json(['result' => 3]); - if ($code == 0) { - $data = $entityManager->getRepository(Commodity::class)->findOneBy([ - 'name' => $params['name'], - 'bid' => $acc['bid'] - ]); - //check exist before - if (!$data) { - $data = new Commodity(); - $data->setCode($provider->getAccountingCode($request->headers->get('activeBid'), 'Commodity')); - } - } else { - $data = $entityManager->getRepository(Commodity::class)->findOneBy([ - 'bid' => $acc['bid'], - 'code' => $code - ]); - if (!$data) - throw $this->createNotFoundException(); + $commodityService = new \App\Cog\CommodityService($entityManager); + $result = $commodityService->addOrUpdateCommodity($params, $acc, $code); + if (isset($result['error'])) { + return $this->json($result, 400); } - if (!array_key_exists('unit', $params)) - $unit = $entityManager->getRepository(CommodityUnit::class)->findAll()[0]; - else - $unit = $entityManager->getRepository(CommodityUnit::class)->findOneBy(['name' => $params['unit']]); - if (!$unit) - throw $this->createNotFoundException('unit not fount!'); - $data->setUnit($unit); - $data->setBid($acc['bid']); - $data->setname($params['name']); - if ($params['khadamat'] == 'true') - $data->setKhadamat(true); - else - $data->setKhadamat(false); - - if (!array_key_exists('withoutTax', $params)) - $data->setWithoutTax(false); - else { - if ($params['withoutTax'] == 'true') - $data->setWithoutTax(true); - else - $data->setWithoutTax(false); - } - - if (array_key_exists('des', $params)) - $data->setDes($params['des']); - - if (array_key_exists('priceSell', $params)) - $data->setPriceSell($params['priceSell']); - - if (array_key_exists('priceBuy', $params)) - $data->setPriceBuy($params['priceBuy']); - - if (array_key_exists('commodityCountCheck', $params)) { - $data->setCommodityCountCheck($params['commodityCountCheck']); - } - if (array_key_exists('barcodes', $params)) { - $data->setBarcodes($params['barcodes']); - } - - if (array_key_exists('taxCode', $params)) { - $data->setTaxCode($params['taxCode']); - } - - if (array_key_exists('taxType', $params)) { - $data->setTaxType($params['taxType']); - } - - if (array_key_exists('taxUnit', $params)) { - $data->setTaxUnit($params['taxUnit']); - } - - if (array_key_exists('minOrderCount', $params)) { - $data->setMinOrderCount($params['minOrderCount']); - } - if (array_key_exists('speedAccess', $params)) { - $data->setSpeedAccess($params['speedAccess']); - } - if (array_key_exists('dayLoading', $params)) { - $data->setDayLoading($params['dayLoading']); - } - if (array_key_exists('orderPoint', $params)) { - $data->setOrderPoint($params['orderPoint']); - } - //set cat - if (array_key_exists('cat', $params)) { - if ($params['cat'] != '') { - if (is_int($params['cat'])) - $cat = $entityManager->getRepository(CommodityCat::class)->find($params['cat']); - else - $cat = $entityManager->getRepository(CommodityCat::class)->find($params['cat']['id']); - if ($cat) { - if ($cat->getBid() == $acc['bid']) { - $data->setCat($cat); - } - } - } - } - $entityManager->persist($data); - - //save prices list - if (array_key_exists('prices', $params)) { - foreach ($params['prices'] as $item) { - $priceList = $entityManager->getRepository(PriceList::class)->findOneBy([ - 'bid' => $acc['bid'], - 'id' => $item['list']['id'] - ]); - if ($priceList) { - $detail = $entityManager->getRepository(PriceListDetail::class)->findOneBy([ - 'list' => $priceList, - 'commodity' => $data - ]); - if (!$detail) { - $detail = new PriceListDetail; - } - $detail->setList($priceList); - $detail->setCommodity($data); - $detail->setPriceSell($item['priceSell']); - $detail->setPriceBuy(0); - $detail->setMoney($acc['money']); - $entityManager->persist($detail); - } - } - } - $entityManager->flush(); $log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid')); - return $this->json([ - 'Success' => true, - 'result' => 1, - 'code' => $data->getId() - ]); + return $this->json($result); } #[Route('/api/commodity/units', name: 'app_commodity_units')] diff --git a/hesabixCore/src/Controller/HesabdariController.php b/hesabixCore/src/Controller/HesabdariController.php index 0aeac3b..d62e910 100644 --- a/hesabixCore/src/Controller/HesabdariController.php +++ b/hesabixCore/src/Controller/HesabdariController.php @@ -848,99 +848,15 @@ class HesabdariController extends AbstractController if ($content = $request->getContent()) { $params = json_decode($content, true); } - if (!array_key_exists('type', $params)) - $this->createNotFoundException(); - $roll = ''; - if ($params['type'] == 'person') - $roll = 'person'; - if ($params['type'] == 'person_receive' || $params['type'] == 'person_send') - $roll = 'person'; - elseif ($params['type'] == 'sell_receive') - $roll = 'sell'; - elseif ($params['type'] == 'bank') - $roll = 'banks'; - elseif ($params['type'] == 'buy_send') - $roll = 'buy'; - elseif ($params['type'] == 'transfer') - $roll = 'bankTransfer'; - elseif ($params['type'] == 'all') - $roll = 'accounting'; - else - $roll = $params['type']; - - $acc = $access->hasRole($roll); + $acc = $access->hasRole($params['type'] ?? 'accounting'); if (!$acc) throw $this->createAccessDeniedException(); - if ($params['type'] == 'person') { - $person = $entityManager->getRepository(Person::class)->findOneBy([ - 'bid' => $acc['bid'], - 'code' => $params['id'], - ]); - if (!$person) - throw $this->createNotFoundException(); - - $data = $entityManager->getRepository(HesabdariRow::class)->findBy([ - 'person' => $person, - ], [ - 'id' => 'DESC' - ]); - } elseif ($params['type'] == 'bank') { - $bank = $entityManager->getRepository(BankAccount::class)->findOneBy([ - 'bid' => $acc['bid'], - 'code' => $params['id'], - ]); - if (!$bank) - throw $this->createNotFoundException(); - - $data = $entityManager->getRepository(HesabdariRow::class)->findBy([ - 'bank' => $bank, - ], [ - 'id' => 'DESC' - ]); - } elseif ($params['type'] == 'cashdesk') { - $cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy([ - 'bid' => $acc['bid'], - 'code' => $params['id'], - ]); - if (!$cashdesk) - throw $this->createNotFoundException(); - - $data = $entityManager->getRepository(HesabdariRow::class)->findBy([ - 'cashdesk' => $cashdesk, - ], [ - 'id' => 'DESC' - ]); - } elseif ($params['type'] == 'salary') { - $salary = $entityManager->getRepository(Salary::class)->findOneBy([ - 'bid' => $acc['bid'], - 'code' => $params['id'], - ]); - if (!$salary) - throw $this->createNotFoundException(); - - $data = $entityManager->getRepository(HesabdariRow::class)->findBy([ - 'salary' => $salary, - ], [ - 'id' => 'DESC' - ]); + $service = new \App\Cog\AccountingDocService($entityManager); + $result = $service->searchRows($params, $acc); + if (isset($result['error'])) { + return $this->json($result, 400); } - $dataTemp = []; - foreach ($data as $item) { - $temp = [ - 'id' => $item->getId(), - 'dateSubmit' => $item->getDoc()->getDateSubmit(), - 'date' => $item->getDoc()->getDate(), - 'type' => $item->getDoc()->getType(), - 'ref' => $item->getRef()->getName(), - 'des' => $item->getDes(), - 'bs' => $item->getBs(), - 'bd' => $item->getBd(), - 'code' => $item->getDoc()->getCode(), - 'submitter' => $item->getDoc()->getSubmitter()->getFullName() - ]; - $dataTemp[] = $temp; - } - return $this->json($dataTemp); + return $this->json($result); } #[Route('/api/accounting/table/get', name: 'app_accounting_table_get')] diff --git a/hesabixCore/src/Controller/PersonsController.php b/hesabixCore/src/Controller/PersonsController.php index 5f69983..538972c 100644 --- a/hesabixCore/src/Controller/PersonsController.php +++ b/hesabixCore/src/Controller/PersonsController.php @@ -238,155 +238,36 @@ class PersonsController extends AbstractController if ($content = $request->getContent()) { $params = json_decode($content, true); } - if (!array_key_exists('nikename', $params)) - return $this->json(['result' => -1]); - if (count_chars(trim($params['nikename'])) == 0) - return $this->json(['result' => 3]); - - if ($code == 0) { - $person = $entityManager->getRepository(Person::class)->findOneBy([ - 'nikename' => $params['nikename'], - 'bid' => $acc['bid'] - ]); - //check exist before - if (!$person) { - $person = new Person(); - $maxAttempts = 10; // حداکثر تعداد تلاش برای تولید کد جدید - $code = null; - - for ($i = 0; $i < $maxAttempts; $i++) { - $code = $provider->getAccountingCode($acc['bid'], 'person'); - $exist = $entityManager->getRepository(Person::class)->findOneBy([ - 'code' => $code - ]); - if (!$exist) { - break; - } - } - - if ($code === null) { - throw new \Exception('نمی‌توان کد جدیدی برای شخص تولید کرد'); - } - - $person->setCode($code); - } - - } else { - $person = $entityManager->getRepository(Person::class)->findOneBy([ - 'bid' => $acc['bid'], - 'code' => $code - ]); - if (!$person) - throw $this->createNotFoundException(); + $personService = new \App\Cog\PersonService($entityManager); + $result = $personService->addOrUpdatePerson($params, $acc, $code); + if (isset($result['error'])) { + return $this->json($result, 400); } - $person->setBid($acc['bid']); - $person->setNikename($params['nikename']); - if (array_key_exists('name', $params)) - $person->setName($params['name']); - if (array_key_exists('birthday', $params)) - $person->setBirthday($params['birthday']); - if (array_key_exists('tel', $params)) - $person->setTel($params['tel']); - if (array_key_exists('speedAccess', $params)) - $person->setSpeedAccess($params['speedAccess']); - if (array_key_exists('address', $params)) - $person->setAddress($params['address']); - if (array_key_exists('des', $params)) - $person->setDes($params['des']); - if (array_key_exists('mobile', $params)) - $person->setMobile($params['mobile']); - if (array_key_exists('mobile2', $params)) - $person->setMobile2($params['mobile2']); - if (array_key_exists('fax', $params)) - $person->setFax($params['fax']); - if (array_key_exists('website', $params)) - $person->setWebsite($params['website']); - if (array_key_exists('email', $params)) - $person->setEmail($params['email']); - if (array_key_exists('postalcode', $params)) - $person->setPostalcode($params['postalcode']); - if (array_key_exists('shahr', $params)) - $person->setShahr($params['shahr']); - if (array_key_exists('ostan', $params)) - $person->setOstan($params['ostan']); - if (array_key_exists('keshvar', $params)) - $person->setKeshvar($params['keshvar']); - if (array_key_exists('sabt', $params)) - $person->setSabt($params['sabt']); - if (array_key_exists('codeeghtesadi', $params)) - $person->setCodeeghtesadi($params['codeeghtesadi']); - if (array_key_exists('shenasemeli', $params)) - $person->setShenasemeli($params['shenasemeli']); - if (array_key_exists('company', $params)) - $person->setCompany($params['company']); - if (array_key_exists('prelabel', $params)) { - if ($params['prelabel'] != '') { - $prelabel = $entityManager->getRepository(PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]); - if ($prelabel) { - $person->setPrelabel($prelabel); - } - } - elseif ($params['prelabel'] == null) { - $person->setPrelabel(null); - } - } - //inset cards - if (array_key_exists('accounts', $params)) { - foreach ($params['accounts'] as $item) { - $card = $entityManager->getRepository(PersonCard::class)->findOneBy([ - 'bid' => $acc['bid'], - 'person' => $person, - 'bank' => $item['bank'] - ]); - if (!$card) - $card = new PersonCard(); - - $card->setPerson($person); - $card->setBid($acc['bid']); - $card->setShabaNum($item['shabaNum']); - $card->setCardNum($item['cardNum']); - $card->setAccountNum($item['accountNum']); - $card->setBank($item['bank']); - $entityManager->persist($card); - } - } - //remove not sended accounts - $accounts = $entityManager->getRepository(PersonCard::class)->findBy([ - 'bid' => $acc['bid'], - 'person' => $person, - ]); - foreach ($accounts as $item) { - $deleted = true; - foreach ($params['accounts'] as $param) { - if ($item->getBank() == $param['bank']) { - $deleted = false; - } - } - if ($deleted) { - $entityManager->remove($item); - } - } - $entityManager->persist($person); - - //insert new types - $types = $entityManager->getRepository(PersonType::class)->findAll(); - foreach ($params['types'] as $item) { - if ($item['checked'] == true) - $person->addType($entityManager->getRepository(PersonType::class)->findOneBy([ - 'code' => $item['code'] - ])); - elseif ($item['checked'] == false) { - $person->removeType($entityManager->getRepository(PersonType::class)->findOneBy([ - 'code' => $item['code'] - ])); - } - } - $entityManager->flush(); $log->insert('اشخاص', 'شخص با نام مستعار ' . $params['nikename'] . ' افزوده/ویرایش شد.', $this->getUser(), $acc['bid']); - return $this->json([ - 'Success' => true, - 'result' => 1, - ]); + return $this->json($result); + } + + #[Route('/api/person/list', name: 'app_persons_list', methods: ['POST'])] + public function app_persons_list( + Provider $provider, + Request $request, + Access $access, + Log $log, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('person'); + if (!$acc) { + var_dump($acc); + throw $this->createAccessDeniedException(); + } + + $params = json_decode($request->getContent(), true) ?? []; + + // استفاده از سرویس جدید + $personService = new \App\Cog\PersonService($entityManager); + $result = $personService->getPersonsList($params, $acc); + + return new JsonResponse($result); } #[Route('/api/person/list/search', name: 'app_persons_list_search')] @@ -478,115 +359,6 @@ class PersonsController extends AbstractController return $this->json($response); } - #[Route('/api/person/list', name: 'app_persons_list', methods: ['POST'])] - public function app_persons_list( - Provider $provider, - Request $request, - Access $access, - Log $log, - EntityManagerInterface $entityManager - ): JsonResponse { - $acc = $access->hasRole('person'); - if (!$acc) { - var_dump($acc); - throw $this->createAccessDeniedException(); - } - - $params = json_decode($request->getContent(), true) ?? []; - $page = $params['page'] ?? 1; - $itemsPerPage = $params['itemsPerPage'] ?? 10; - $search = $params['search'] ?? ''; - $types = $params['types'] ?? null; - $transactionFilters = $params['transactionFilters'] ?? null; - - // کوئری اصلی برای گرفتن همه اشخاص - $queryBuilder = $entityManager->getRepository(\App\Entity\Person::class) - ->createQueryBuilder('p') - ->where('p.bid = :bid') - ->setParameter('bid', $acc['bid']); - - // جست‌وجو (بهبود داده‌شده) - if (!empty($search) || $search === '0') { // برای اطمینان از کار با "0" یا خالی - $search = trim($search); // حذف فضای خالی اضافی - $queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search') - ->setParameter('search', "%$search%"); - } - - // فیلتر نوع اشخاص - if ($types && !empty($types)) { - $queryBuilder->leftJoin('p.type', 't') - ->andWhere('t.code IN (:types)') - ->setParameter('types', $types); - } - - // تعداد کل (قبل از فیلتر تراکنش‌ها) - $totalItems = (clone $queryBuilder) - ->select('COUNT(p.id)') - ->getQuery() - ->getSingleScalarResult(); - - // گرفتن اشخاص با صفحه‌بندی - $persons = $queryBuilder - ->select('p') - ->setFirstResult(($page - 1) * $itemsPerPage) - ->setMaxResults($itemsPerPage) - ->getQuery() - ->getResult(); - - // محاسبه تراکنش‌ها و اعمال فیلتر تراکنش‌ها - $response = []; - foreach ($persons as $person) { - $rows = $entityManager->getRepository(\App\Entity\HesabdariRow::class)->findBy([ - 'person' => $person, - 'bid' => $acc['bid'] - ]); - $bs = 0; // بستانکار - $bd = 0; // بدهکار - foreach ($rows as $row) { - $doc = $row->getDoc(); - if ($doc && $doc->getMoney() && $doc->getYear() && - $doc->getMoney()->getId() == $acc['money']->getId() && - $doc->getYear()->getId() == $acc['year']->getId()) { - $bs += (float) $row->getBs(); // بستانکار - $bd += (float) $row->getBd(); // بدهکار - } - } - $balance = $bs - $bd; // تراز = بستانکار - بدهکار - - // اعمال فیلتر transactionFilters - $include = true; - if ($transactionFilters && !empty($transactionFilters)) { - $include = false; - if (in_array('debtors', $transactionFilters) && $balance < 0) { // بدهکارها (تراز منفی) - $include = true; - } - if (in_array('creditors', $transactionFilters) && $balance > 0) { // بستانکارها (تراز مثبت) - $include = true; - } - if (in_array('zero', $transactionFilters) && $balance == 0) { // تسویه‌شده‌ها - $include = true; - } - } - - if ($include) { - $result = Explore::ExplorePerson($person, $entityManager->getRepository(PersonType::class)->findAll()); - $result['bs'] = $bs; - $result['bd'] = $bd; - $result['balance'] = $balance; - $response[] = $result; - } - } - - // تعداد آیتم‌های فیلترشده - $filteredTotal = count($response); - - return new JsonResponse([ - 'items' => array_slice($response, 0, $itemsPerPage), // فقط تعداد درخواستی - 'total' => $filteredTotal, // تعداد کل فیلترشده - 'unfilteredTotal' => $totalItems, // تعداد کل بدون فیلتر (اختیاری) - ]); - } - #[Route('/api/person/list/debtors/{amount}', name: 'app_persons_list_debtors')] public function app_persons_list_debtors(string $amount, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse { diff --git a/hesabixCore/src/Controller/wizardController.php b/hesabixCore/src/Controller/wizardController.php index 29e94d4..4279890 100644 --- a/hesabixCore/src/Controller/wizardController.php +++ b/hesabixCore/src/Controller/wizardController.php @@ -29,7 +29,8 @@ class wizardController extends AbstractController Request $request, Access $access, EntityManagerInterface $entityManager, - Log $log + Log $log, + \App\Service\registryMGR $registryMGR ): JsonResponse { @@ -73,6 +74,53 @@ class wizardController extends AbstractController $options = $params['options'] ?? []; $conversationId = $params['conversationId'] ?? null; + // بررسی امنیتی conversationId + if ($conversationId) { + $conversation = $entityManager->getRepository(AIConversation::class)->find($conversationId); + if ($conversation) { + // بررسی دسترسی کاربر به این گفتگو + if ($conversation->getUser()->getId() !== $acc['user']->getId() || + $conversation->getBusiness()->getId() !== $acc['bid']->getId()) { + + $log->warning('تلاش غیرمجاز برای دسترسی به گفتگوی دیگران در wizard_talk', [ + 'conversationId' => $conversationId, + 'requestedUser' => $acc['user']->getId(), + 'requestedBusiness' => $acc['bid']->getId(), + 'conversationUser' => $conversation->getUser()->getId(), + 'conversationBusiness' => $conversation->getBusiness()->getId() + ]); + + return $this->json([ + 'success' => false, + 'error' => 'دسترسی غیرمجاز به گفتگو', + 'debug_info' => [ + 'conversationId' => $conversationId + ] + ]); + } + + // بررسی حذف شدن گفتگو + if ($conversation->isDeleted()) { + $log->info('تلاش برای دسترسی به گفتگوی حذف شده در wizard_talk', [ + 'conversationId' => $conversationId, + 'user' => $acc['user']->getId() + ]); + + // گفتگوی جدید ایجاد می‌شود + $conversationId = null; + } + } else { + $log->warning('تلاش برای دسترسی به گفتگوی ناموجود در wizard_talk', [ + 'conversationId' => $conversationId, + 'user' => $acc['user']->getId(), + 'business' => $acc['bid']->getId() + ]); + + // گفتگوی جدید ایجاد می‌شود + $conversationId = null; + } + } + // بررسی فعال بودن هوش مصنوعی $aiStatus = $this->agiService->checkAIServiceStatus(); if (!$aiStatus['isEnabled']) { @@ -110,19 +158,31 @@ class wizardController extends AbstractController // استفاده از AGIService برای مدیریت گفتگو و ارسال درخواست $result = $this->agiService->sendRequest($message, $business, $acc['user'], $conversationId, $acc); + // دریافت وضعیت نمایش دیباگ از تنظیمات سیستم + $aiDebugMode = false; + if ($registryMGR) { + $aiDebugMode = $registryMGR->get('system', 'aiDebugMode') === '1' || $registryMGR->get('system', 'aiDebugMode') === true; + } + if ($result['success']) { $responseContent = $result['response'] ?? $result['message'] ?? 'عملیات با موفقیت انجام شد'; $response = [ 'success' => true, 'response' => $responseContent, - 'conversationId' => $result['conversation_id'] ?? null, + 'conversationId' => $result['conversationId'] ?? $result['conversation_id'] ?? null, 'model' => $result['model'] ?? null, 'usage' => $result['usage'] ?? null, 'cost' => $result['cost'] ?? null, 'debug_info' => $result['debug_info'] ?? null ]; + // اگر دیباگ خاموش بود، debug_info و model را حذف کن + if (!$aiDebugMode) { + unset($response['debug_info']); + unset($response['model']); + } + // محاسبه هزینه در صورت وجود اطلاعات usage if (isset($result['cost'])) { $cost = $result['cost']; @@ -217,5 +277,138 @@ class wizardController extends AbstractController } } + #[Route('/api/wizard/conversations/list', name: 'wizard_conversations_list', methods: ['POST'])] + public function wizard_conversations_list(Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\Jdate $jdate): JsonResponse + { + $acc = $access->hasRole('join'); + if (!$acc) { + return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']); + } + $params = json_decode($request->getContent(), true) ?? []; + $search = $params['search'] ?? ''; + $category = $params['category'] ?? ''; + $conversationRepo = $entityManager->getRepository(\App\Entity\AIConversation::class); + if (!empty($search)) { + $conversations = $conversationRepo->searchByTitle($acc['user'], $acc['bid'], $search); + } elseif (!empty($category)) { + $conversations = $conversationRepo->findByCategory($acc['user'], $acc['bid'], $category); + } else { + $conversations = $conversationRepo->findActiveConversations($acc['user'], $acc['bid']); + } + $result = []; + foreach ($conversations as $conversation) { + $messageRepo = $entityManager->getRepository(\App\Entity\AIMessage::class); + $lastMessage = $messageRepo->findLastMessageByConversation($conversation); + $result[] = [ + 'id' => $conversation->getId(), + 'title' => $conversation->getTitle(), + 'category' => $conversation->getCategory(), + 'createdAt' => $jdate->jdate('Y/m/d H:i', $conversation->getCreatedAt()), + 'updatedAt' => $jdate->jdate('Y/m/d H:i', $conversation->getUpdatedAt()), + 'messageCount' => count($conversation->getMessages()), + 'lastMessage' => $lastMessage ? $lastMessage->getContent() : '' + ]; + } + return $this->json(['success' => true, 'items' => $result]); + } + + #[Route('/api/wizard/conversations/create', name: 'wizard_conversations_create', methods: ['POST'])] + public function wizard_conversations_create(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('join'); + if (!$acc) { + return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']); + } + $params = json_decode($request->getContent(), true) ?? []; + $title = $params['title'] ?? 'گفتگوی جدید'; + $category = $params['category'] ?? 'عمومی'; + $conversation = new \App\Entity\AIConversation(); + $conversation->setUser($acc['user']); + $conversation->setBusiness($acc['bid']); + $conversation->setTitle($title); + $conversation->setCategory($category); + $entityManager->persist($conversation); + $entityManager->flush(); + return $this->json(['success' => true, 'id' => $conversation->getId(), 'title' => $conversation->getTitle(), 'category' => $conversation->getCategory(), 'createdAt' => $conversation->getCreatedAt()]); + } + + #[Route('/api/wizard/conversations/{id}/delete', name: 'wizard_conversations_delete', methods: ['POST'])] + public function wizard_conversations_delete(int $id, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('join'); + if (!$acc) { + return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']); + } + $conversation = $entityManager->getRepository(\App\Entity\AIConversation::class)->find($id); + if (!$conversation) { + return $this->json(['success' => false, 'error' => 'گفتگو یافت نشد']); + } + if ($conversation->getUser()->getId() !== $acc['user']->getId() || $conversation->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']); + } + if ($conversation->isDeleted()) { + return $this->json(['success' => false, 'error' => 'این گفتگو قبلاً حذف شده است']); + } + $conversation->setDeleted(true); + $entityManager->persist($conversation); + $entityManager->flush(); + return $this->json(['success' => true]); + } + + #[Route('/api/wizard/conversations/{id}/messages', name: 'wizard_conversations_messages', methods: ['POST'])] + public function wizard_conversations_messages(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\Jdate $jdate): JsonResponse + { + $acc = $access->hasRole('join'); + if (!$acc) { + return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']); + } + $conversation = $entityManager->getRepository(\App\Entity\AIConversation::class)->find($id); + if (!$conversation) { + return $this->json(['success' => false, 'error' => 'گفتگو یافت نشد']); + } + if ($conversation->getUser()->getId() !== $acc['user']->getId() || $conversation->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']); + } + $messageRepo = $entityManager->getRepository(\App\Entity\AIMessage::class); + $messages = $messageRepo->findByConversation($conversation); + $result = []; + foreach ($messages as $message) { + $result[] = [ + 'id' => $message->getId(), + 'role' => $message->getRole(), + 'content' => $message->getContent(), + 'createdAt' => $jdate->jdate('Y/m/d H:i', $message->getCreatedAt()) + ]; + } + return $this->json(['success' => true, 'items' => $result]); + } + + #[Route('/api/wizard/conversations/delete-all', name: 'wizard_conversations_delete_all', methods: ['POST'])] + public function wizard_conversations_delete_all(Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('join'); + if (!$acc) { + return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']); + } + $userId = $acc['user']->getId(); + $businessId = $acc['bid']->getId(); + $repo = $entityManager->getRepository(AIConversation::class); + $convs = $repo->createQueryBuilder('c') + ->where('c.user = :user') + ->andWhere('c.business = :business') + ->andWhere('c.deleted = false') + ->setParameter('user', $userId) + ->setParameter('business', $businessId) + ->getQuery()->getResult(); + $count = 0; + foreach ($convs as $conv) { + $conv->setDeleted(true); + $entityManager->persist($conv); + $count++; + } + $entityManager->flush(); + return $this->json(['success' => true, 'deleted' => $count]); + } + } diff --git a/hesabixCore/src/Service/AGI/AGIService.php b/hesabixCore/src/Service/AGI/AGIService.php index 203f0b9..b2b4e94 100644 --- a/hesabixCore/src/Service/AGI/AGIService.php +++ b/hesabixCore/src/Service/AGI/AGIService.php @@ -102,6 +102,11 @@ class AGIService $result = $this->sendToAIServiceWithFunctionCalling($userPrompt, $apiKey, $service, $conversationHistory, $acc); if (!$result['success']) { + // اگر system_prompt در debug_info وجود داشت، به خروجی خطا اضافه کن + if (isset($result['debug_info']['system_prompt'])) { + if (!isset($result['debug_info'])) $result['debug_info'] = []; + $result['debug_info']['system_prompt'] = $result['debug_info']['system_prompt']; + } return $result; } @@ -112,19 +117,25 @@ class AGIService // ذخیره پاسخ هوش مصنوعی $this->saveAIMessage($conversation, $aiResponse, $result['data'], $cost); + $debugInfo = [ + 'context' => 'sendRequest', + 'function_calls' => $result['function_calls'] ?? [], + 'tool_results' => $result['tool_results'] ?? [], + 'conversation_history' => $conversationHistory + ]; + // اگر system_prompt در debug_info خروجی قبلی بود، به debug_info اضافه کن + if (isset($result['debug_info']['system_prompt'])) { + $debugInfo['system_prompt'] = $result['debug_info']['system_prompt']; + } return [ 'success' => true, 'response' => $aiResponse, - 'conversationId' => $conversation->getId(), + 'conversationId' => $conversation->getId(), // مقداردهی صحیح conversationId 'model' => $this->getAIModel(), 'usage' => $result['data']['usage'] ?? [], 'cost' => $cost, - 'debug_info' => [ - 'context' => 'sendRequest', - 'function_calls' => $result['function_calls'] ?? [], - 'tool_results' => $result['tool_results'] ?? [] - ] - ]; + 'debug_info' => $debugInfo + ]; try { } catch (\Exception $e) { return [ @@ -149,24 +160,15 @@ class AGIService { $urls = $this->getServiceUrls($service); $model = $this->getAIModel(); - - // پیام system شامل قوانین خروجی و مثال دقیق - $systemPrompt = "شما دستیار هوشمند حسابیکس هستید. فقط پاسخ را به صورت JSON مطابق مثال خروجی بده. اگر نیاز به ابزار داشتی، از function calling استفاده کن." - . $this->promptService->getOutputFormatPrompt(); + // ساخت پرامپت system با buildSmartPrompt + $systemPrompt = $this->buildSmartPrompt($prompt, $acc['bid'] ?? null, $conversationHistory); $messages = [ [ 'role' => 'system', 'content' => $systemPrompt ] ]; - // تاریخچه گفتگو - foreach ($conversationHistory as $historyItem) { - $messages[] = [ - 'role' => $historyItem['role'], - 'content' => $historyItem['content'] - ]; - } - // پیام user فقط سوال فعلی + // تاریخچه گفتگو و پیام user دیگر اینجا اضافه نمی‌شود چون در buildSmartPrompt لحاظ شده است $messages[] = [ 'role' => 'user', 'content' => $prompt @@ -212,7 +214,8 @@ class AGIService 'apiKey' => $apiKey, 'service' => $service, 'conversationHistory' => $conversationHistory, - + 'system_prompt' => $systemPrompt, + 'messages_to_ai' => $messages, ] ]; } @@ -227,7 +230,9 @@ class AGIService 'debug_info' => [ 'context' => 'sendToAIServiceWithFunctionCalling', 'response_data' => $responseData, - 'iteration' => $iteration + 'iteration' => $iteration, + 'system_prompt' => $systemPrompt, + 'messages_to_ai' => $messages, ] ]; } @@ -242,7 +247,11 @@ class AGIService 'success' => true, 'data' => $responseData, 'function_calls' => $functionCalls, - 'tool_results' => $toolResults + 'tool_results' => $toolResults, + 'debug_info' => [ + 'system_prompt' => $systemPrompt, + 'messages_to_ai' => $messages, + ] ]; } @@ -292,7 +301,9 @@ class AGIService 'context' => 'sendToAIServiceWithFunctionCalling', 'max_iterations' => $maxIterations, 'function_calls' => $functionCalls, - 'tool_results' => $toolResults + 'tool_results' => $toolResults, + 'system_prompt' => $systemPrompt, + 'messages_to_ai' => $messages, ] ]; } @@ -305,10 +316,25 @@ class AGIService try { switch ($tool) { case 'getPersonInfo': - // استفاده مستقیم از سرویس جدید - $cogPersonService = new \App\Cog\PersonService($this->em, $params['acc'] ?? null); + $cogPersonService = new \App\Cog\PersonService($this->em); $personService = new \App\AiTool\PersonService($this->em, $cogPersonService); return $personService->getPersonInfoByCode($params['code'] ?? null, $params['acc'] ?? null); + case 'getPersonsList': + $cogPersonService = new \App\Cog\PersonService($this->em); + $personService = new \App\AiTool\PersonService($this->em, $cogPersonService); + return $personService->getPersonsListAi($params, $params['acc'] ?? null); + case 'addOrUpdatePerson': + $cogPersonService = new \App\Cog\PersonService($this->em); + $personService = new \App\AiTool\PersonService($this->em, $cogPersonService); + return $personService->addOrUpdatePersonAi($params, $params['acc'] ?? null, $params['code'] ?? 0); + case 'addOrUpdateCommodity': + $cogCommodityService = new \App\Cog\CommodityService($this->em); + $commodityService = new \App\AiTool\CommodityService($this->em, $cogCommodityService); + return $commodityService->addOrUpdateCommodityAi($params, $params['acc'] ?? null, $params['code'] ?? 0); + case 'searchAccountingRows': + $cogAccountingDocService = new \App\Cog\AccountingDocService($this->em); + $accountingDocService = new \App\AiTool\AccountingDocService($this->em, $cogAccountingDocService); + return $accountingDocService->searchRowsAi($params, $params['acc'] ?? null); default: return [ 'error' => 'ابزار ناشناخته: ' . $tool @@ -326,28 +352,41 @@ class AGIService */ private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string { - // دریافت پرامپ‌های پایه از PromptService - $basePrompts = $this->promptService->getAllBasePrompts(); - $prompt = $basePrompts; + // دریافت aiPrompt مدیر سیستم و قوانین خروجی + $aiPrompt = $this->registryMGR->get('system', 'aiPrompt'); + $outputFormatPrompt = $this->promptService->getOutputFormatPrompt(); + $basePrompt = "شما دستیار هوشمند حسابیکس هستید. فقط پاسخ را به صورت JSON مطابق مثال خروجی بده. اگر نیاز به ابزار داشتی، از function calling استفاده کن."; - // قوانین خروجی JSON و مثال‌ها از سرویس مدیریت پرامپت‌ها - $prompt .= $this->promptService->getOutputFormatPrompt(); + $parts = []; + // اضافه کردن aiPrompt اگر تکراری نبود + if ($aiPrompt && strpos($basePrompt, trim($aiPrompt)) === false && strpos($outputFormatPrompt, trim($aiPrompt)) === false) { + $parts[] = trim($aiPrompt); + } + // اضافه کردن basePrompt اگر تکراری نبود + if (strpos($outputFormatPrompt, trim($basePrompt)) === false) { + $parts[] = trim($basePrompt); + } + // قوانین خروجی اگر تکراری نبود + $parts[] = trim($outputFormatPrompt); // اضافه کردن اطلاعات کسب و کار if ($business) { - $prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}."; + $parts[] = "اطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}."; } // اضافه کردن تاریخچه گفتگو if (!empty($conversationHistory)) { - $prompt .= "\n\n📜 تاریخچه گفتگو:\n"; + $historyText = "\n📜 تاریخچه گفتگو:\n"; foreach ($conversationHistory as $historyItem) { $role = $historyItem['role'] === 'user' ? 'کاربر' : 'دستیار'; - $prompt .= "{$role}: {$historyItem['content']}\n"; + $historyText .= "{$role}: {$historyItem['content']}\n"; } - $prompt .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید."; + $historyText .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید."; + $parts[] = $historyText; } - $prompt .= "\n\nسوال کاربر: " . $message; - return $prompt; + // سوال کاربر + $parts[] = "سوال کاربر: " . $message; + // ادغام نهایی + return implode("\n\n", array_filter($parts)); } /** @@ -604,8 +643,25 @@ class AGIService if ($conversationId) { // بازیابی گفتگوی موجود $conversation = $this->em->getRepository(AIConversation::class)->find($conversationId); - if ($conversation && $conversation->getUser() === $user && $conversation->getBusiness() === $business) { - // به‌روزرسانی زمان آخرین تغییر + + // بررسی وجود گفتگو + if (!$conversation) { + // ایجاد گفتگوی جدید به جای خطا + } + // بررسی دسترسی کاربر به گفتگو + elseif ($conversation->getUser()->getId() !== $user->getId() || + $conversation->getBusiness()->getId() !== $business->getId()) { + // ایجاد گفتگوی جدید به جای خطا + $conversation = null; + } + // بررسی حذف شدن گفتگو + elseif ($conversation->isDeleted()) { + // ایجاد گفتگوی جدید به جای خطا + $conversation = null; + } + + // اگر گفتگو معتبر است، به‌روزرسانی زمان + if ($conversation && !$conversation->isDeleted()) { $conversation->setUpdatedAt(time()); $this->em->persist($conversation); return $conversation; @@ -624,6 +680,8 @@ class AGIService $conversation->setDeleted(false); $this->em->persist($conversation); + $this->em->flush(); // اضافه شد تا id مقداردهی شود + return $conversation; } @@ -758,7 +816,36 @@ class AGIService { $conversation = $this->em->getRepository(AIConversation::class)->find($conversationId); - if (!$conversation || $conversation->getUser() !== $user || $conversation->getBusiness() !== $business) { + // بررسی وجود گفتگو + if (!$conversation) { + $this->log->warning('تلاش برای دریافت پیام‌های گفتگوی ناموجود', [ + 'conversationId' => $conversationId, + 'user' => $user ? $user->getId() : 'unknown', + 'business' => $business ? $business->getId() : 'unknown' + ]); + return []; + } + + // بررسی دسترسی کاربر به گفتگو + if ($conversation->getUser()->getId() !== $user->getId() || + $conversation->getBusiness()->getId() !== $business->getId()) { + + $this->log->warning('تلاش غیرمجاز برای دریافت پیام‌های گفتگوی دیگران', [ + 'conversationId' => $conversationId, + 'requestedUser' => $user ? $user->getId() : 'unknown', + 'requestedBusiness' => $business ? $business->getId() : 'unknown', + 'conversationUser' => $conversation->getUser()->getId(), + 'conversationBusiness' => $conversation->getBusiness()->getId() + ]); + return []; + } + + // بررسی حذف شدن گفتگو + if ($conversation->isDeleted()) { + $this->log->info('تلاش برای دریافت پیام‌های گفتگوی حذف شده', [ + 'conversationId' => $conversationId, + 'user' => $user ? $user->getId() : 'unknown' + ]); return []; } diff --git a/hesabixCore/src/Service/AGI/Promps/AccountingDocPromptService.php b/hesabixCore/src/Service/AGI/Promps/AccountingDocPromptService.php new file mode 100644 index 0000000..ae6cf9d --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/AccountingDocPromptService.php @@ -0,0 +1,93 @@ +em = $entityManager; + } + /** + * ابزارهای بخش اسناد حسابداری برای function calling + */ + public function getTools(): array + { + $tools = []; + // ابزار جست‌وجوی ردیف‌های اسناد + $searchRowsPrompt = $this->getSearchAccountingRowsPrompt(); + $searchRowsData = json_decode($searchRowsPrompt, true); + if ($searchRowsData) { + $tools[] = [ + 'type' => 'function', + 'function' => [ + 'name' => $searchRowsData['tool'], + 'description' => $searchRowsData['description'], + 'parameters' => $searchRowsData['parameters'] + ] + ]; + } + return $tools; + } + + /** + * پرامپ‌های توضیحی برای مدل + */ + public function getAllAccountingDocPrompts(): string + { + return $this->getSearchAccountingRowsPrompt(); + } + + public function getSearchAccountingRowsPrompt(): string + { + return '{ + "tool": "searchAccountingRows", + "description": "این ابزار برای جست‌وجوی تراکنش‌ها و ردیف‌های اسناد حسابداری اشخاص، حساب‌های بانکی، صندوق، حقوق و ... استفاده می‌شود. برای استفاده، ابتدا باید با ابزارهای جست‌وجوی اشخاص یا حساب‌های بانکی و ...، کد (code) یا شناسه مورد نظر را به دست آورید. سپس با ارسال نوع (type) مناسب (مانند person، bank، cashdesk، salary و ...) و کد (id)، می‌توانید لیست تراکنش‌ها یا ردیف‌های مرتبط را دریافت کنید. خروجی شامل لیست ردیف‌ها با اطلاعات کامل است.", + "endpoint": "/api/accounting/rows/search", + "method": "POST", + "parameters": { + "type": "object", + "properties": { + "type": {"type": "string", "description": "نوع جست‌وجو (person, bank, cashdesk, salary و ...)"}, + "id": {"type": ["string", "integer"], "description": "کد یا شناسه مورد جست‌وجو"}, + "acc": {"type": "object", "description": "اطلاعات دسترسی (الزامی برای بک‌اند)"} + }, + "required": ["type", "id"] + }, + "output": [ + { + "id": "integer", + "dateSubmit": "string", + "date": "string", + "type": "string", + "ref": "string", + "des": "string", + "bs": "string", + "bd": "string", + "code": "string", + "submitter": "string" + } + ], + "examples": { + "input": {"type":"person","id":"1001","acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}}, + "output": [ + { + "id": 9, + "dateSubmit": "1753370911", + "date": "1404/05/02", + "type": "sell", + "ref": "حساب‌های دریافتی", + "des": "فاکتور فروش", + "bs": "0", + "bd": "210000", + "code": "1000", + "submitter": "بابک" + } + ] + } +}'; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/BasePromptService.php b/hesabixCore/src/Service/AGI/Promps/BasePromptService.php index a7665ae..fbf8548 100644 --- a/hesabixCore/src/Service/AGI/Promps/BasePromptService.php +++ b/hesabixCore/src/Service/AGI/Promps/BasePromptService.php @@ -5,130 +5,35 @@ namespace App\Service\AGI\Promps; use Doctrine\ORM\EntityManagerInterface; use App\Service\Access; use App\Entity\APIToken; +use App\Service\registryMGR; class BasePromptService { private $em; private $access; + private $registryMGR; - public function __construct(EntityManagerInterface $entityManager, Access $access) - { + public function __construct( + EntityManagerInterface $entityManager, + Access $access, + registryMGR $registryMGR + ) { $this->em = $entityManager; $this->access = $access; + $this->registryMGR = $registryMGR; } /** - * پرامپ پایه برای معرفی سیستم - * @return string - */ - public function getSystemIntroductionPrompt(): string - { - // دسترسی فعلی - $acc = $this->access->hasRole('join'); - $apiToken = null; - if ($acc && isset($acc['bid']) && isset($acc['user'])) { - // جستجوی توکن AI معتبر برای این کاربر و کسب‌وکار - $now = time(); - $apiToken = $this->em->getRepository(APIToken::class)->findOneBy([ - 'bid' => $acc['bid'], - 'submitter' => $acc['user'], - 'isForAi' => true - ]); - if ($apiToken) { - $expire = $apiToken->getDateExpire(); - if ($expire && $expire != '0' && $now > (int)$expire) { - $apiToken = null; // منقضی شده - } - } - if (!$apiToken) { - // ساخت توکن جدید با اعتبار ۳۰ دقیقه - $apiToken = $this->access->createAiToken($acc['bid'], $acc['user'], 1800); - } - } - $apiKey = $apiToken ? $apiToken->getToken() : ''; - return '{ - "tool": "system_introduction", - "description": "System introduction and authentication requirements", - "content": "You are an AI assistant for Hesabix accounting system. This system manages businesses, persons, accounting entries, inventory, and financial reports. You can help users with various tasks using the available tools and APIs.", - "capabilities": [ - "Person management (customers, suppliers, employees)", - ], - "authentication": { - "method": "API Key or Session Token", - "required_headers": { - "api-key": "' . $apiKey . ' (این کد را در هدر api-key قرار بده)", - }, - }, - "language": "Persian (فارسی)", - "currency": "Iranian Rial (ریال)" -}'; - } - - /** - * پرامپ پایه برای خطاها + * پرامپ پایه تحلیلی و استدلالی برای هوش مصنوعی * @return string */ public function getErrorHandlingPrompt(): string { return '{ - "tool": "error_handling", - "description": "Error handling and authentication guidance", - "instructions": "When encountering errors, provide clear explanations in Persian and suggest solutions. Focus on authentication and access control issues.", - "error_types": { - "access_denied": "دسترسی غیرمجاز - کاربر فاقد مجوز لازم است", - "ai_permission_denied": "دسترسی هوش مصنوعی غیرمجاز - کاربر فاقد مجوز AI است", - "invalid_api_key": "کلید API نامعتبر - لطفاً کلید صحیح را وارد کنید", - "expired_token": "توکن منقضی شده - لطفاً توکن جدید دریافت کنید", - "invalid_business": "کسب و کار نامعتبر - شناسه کسب و کار صحیح نیست", - "invalid_year": "سال مالی نامعتبر - سال مالی انتخاب شده صحیح نیست", - "invalid_currency": "واحد پول نامعتبر - واحد پول انتخاب شده صحیح نیست", - "not_found": "مورد یافت نشد - کد یا شناسه وارد شده صحیح نیست", - "validation_error": "خطای اعتبارسنجی - اطلاعات وارد شده صحیح نیست", - "network_error": "خطای شبکه - اتصال اینترنت را بررسی کنید" - }, - "authentication_solutions": { - "missing_api_key": "کلید API را در هدر api-key قرار دهید", - "expired_token": "توکن جدید از مدیر سیستم دریافت کنید", - "no_ai_permission": "مجوز هوش مصنوعی از مدیر کسب و کار دریافت کنید", - "wrong_business": "شناسه کسب و کار صحیح را در هدر activeBid قرار دهید", - "wrong_year": "سال مالی صحیح را در هدر activeYear قرار دهید" - }, - "response_format": "Explain error in Persian, provide specific solution, and suggest next steps" -}'; - } - - /** - * پرامپ پایه برای راهنمایی کاربر - * @return string - */ - public function getHelpPrompt(): string - { - return '{ - "tool": "help", - "description": "User help and guidance", - "instructions": "Provide helpful guidance to users about available features and how to use them. Be concise and clear in Persian.", - "common_queries": { - "person_info": "برای دریافت اطلاعات شخص، کد شخص را وارد کنید", - }, - "response_format": "Provide step-by-step guidance in Persian with examples" -}'; - } - - /** - * پرامپ برای نمایش دامنه اصلی API - * @return string - */ - public function getApiBaseUrlPrompt(): string - { - // دریافت اولین رکورد تنظیمات - $settings = $this->em->getRepository(\App\Entity\Settings::class)->findAll(); - $appSite = isset($settings[0]) ? $settings[0]->getAppSite() : ''; - $domain = $appSite ? $appSite : '---'; - return '{ - "tool": "api_base_url", - "description": "آدرس پایه API", - "content": "تمام اندپوینت‌های سیستم از طریق دامنه زیر قابل دسترسی هستند:", - "base_url": "' . $domain . '" + "tool": "reasoning_base", + "description": "شما یک دستیار هوشمند حسابداری هستید که باید مانند یک فیلسوف و تحلیل‌گر رفتار کنید. هدف شما فقط اجرای مستقیم دستورات نیست، بلکه باید هدف نهایی کاربر را از دل مکالمه و تاریخچه بفهمید و برای رسیدن به آن، ابزار مناسب را انتخاب کنید. ممکن است برای رسیدن به هدف، نیاز باشد ابتدا با یک ابزار (مثلاً جست‌وجوی اشخاص یا کالا) داده‌ای (مثل code یا id) را به دست آورید و سپس آن را به عنوان ورودی ابزار دیگر (مثلاً ویرایش یا جست‌وجوی تراکنش) استفاده کنید. همیشه تحلیل کن که چه داده‌ای نیاز است و چه ابزاری باید فراخوانی شود. در حسابداری، code (کد) و id (شناسه دیتابیس) دو مفهوم کاملاً متفاوت هستند و نباید به جای هم استفاده شوند. اگر کاربر در چند پیام متوالی صحبت کرد (مثلاً ابتدا گفت بابک را پیدا کن و بعد گفت تلفنش را ویرایش کن)، باید از تاریخچه مکالمه بفهمی منظورش تغییر تلفن همان شخص بابک است و ابزار مناسب را با داده صحیح فراخوانی کنی. همیشه سعی کن با تحلیل و استدلال چندمرحله‌ای، بهترین مسیر را برای حل مسئله انتخاب کنی و اگر نیاز به پرسش از کاربر بود، سؤال شفاف و هدفمند بپرس. اگر داده‌ای ناقص بود، با تحلیل تاریخچه یا پرسش از کاربر آن را کامل کن. در تحلیل داده‌های حسابداری، دقت و صحت اطلاعات بسیار مهم است و هرگونه اشتباه در تشخیص ابزار یا داده می‌تواند منجر به خطای مالی شود. خروجی هر ابزار را به دقت بررسی کن و اگر نیاز بود، آن را به عنوان ورودی ابزار بعدی استفاده کن. همیشه به دنبال درک عمیق‌تر هدف کاربر و ارائه راه‌حل بهینه باش.", + "instructions": "استدلال کن، تحلیل کن، ابزار مناسب را انتخاب کن، از تاریخچه استفاده کن، تفاوت code و id را رعایت کن، خروجی ابزارها را به هم متصل کن و مانند یک متفکر حرفه‌ای عمل کن.", + "response_format": "Explain your reasoning in Persian, select the best tool, and if needed, ask clarifying questions." }'; } @@ -139,12 +44,21 @@ class BasePromptService public function getAllBasePrompts(): string { $prompts = []; - - $prompts[] = $this->getSystemIntroductionPrompt(); + $aiPrompt = $this->registryMGR->get('system', 'aiPrompt'); + if ($aiPrompt) { + $prompts[] = $aiPrompt; + } $prompts[] = $this->getErrorHandlingPrompt(); - $prompts[] = $this->getHelpPrompt(); - $prompts[] = $this->getApiBaseUrlPrompt(); - return implode("\n\n", $prompts); } + + /** + * دریافت تمام ابزارهای پایه + * @return array + */ + public function getAllTools(): array + { + $tools = []; + return $tools; + } } \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php b/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php index 8a9c990..a9a8fa6 100644 --- a/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php +++ b/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php @@ -13,111 +13,71 @@ class InventoryPromptService $this->em = $entityManager; } - /** - * دریافت تمام ابزارهای بخش کالاها برای function calling - * @return array - */ public function getTools(): array { $tools = []; - - // ابزار getItemInfo - $itemInfoPrompt = $this->getItemInfoPrompt(); - $itemInfoData = json_decode($itemInfoPrompt, true); - - if ($itemInfoData) { - // اصلاح ساختار properties - $properties = [ - 'code' => [ - 'type' => 'string', - 'description' => 'Item code (e.g., 1001, 1002)' - ] - ]; + $commodityPrompt = $this->getAddOrUpdateCommodityPrompt(); + $commodityData = json_decode($commodityPrompt, true); + if ($commodityData) { $tools[] = [ 'type' => 'function', 'function' => [ - 'name' => $itemInfoData['tool'], - 'description' => $itemInfoData['description'], - 'parameters' => [ - 'type' => 'object', - 'properties' => $properties, - 'required' => ['code'] - ] + 'name' => $commodityData['tool'], + 'description' => $commodityData['description'], + 'parameters' => $commodityData['parameters'] ] ]; } - return $tools; } - /** - * تولید تمام پرامپ‌های بخش کالاها - * @return string - */ public function getAllInventoryPrompts(): string { - $prompts = []; - - // اضافه کردن تمام پرامپ‌های موجود - $prompts[] = $this->getItemInfoPrompt(); - - // در آینده پرامپ‌های دیگر اضافه خواهند شد - // $prompts[] = $this->getCreateItemPrompt(); - // $prompts[] = $this->getUpdateItemPrompt(); - // $prompts[] = $this->getSearchItemPrompt(); - // $prompts[] = $this->getItemStockPrompt(); - - // ترکیب تمام پرامپ‌ها - return implode("\n\n", $prompts); + return $this->getAddOrUpdateCommodityPrompt(); } - - /** - * پرامپ برای دریافت اطلاعات کامل کالا - * @return string - */ - public function getItemInfoPrompt(): string + + public function getAddOrUpdateCommodityPrompt(): string { return '{ - "tool": "getItemInfo", - "description": "Get complete item information by code", - "endpoint": "/api/item/info/{code}", - "method": "GET", - "input": { - "code": "string - Item code (e.g., 1001, 1002)" - }, - "output": { - "id": "integer - Item ID", - "code": "string - Item code", - "name": "string - Item name", - "description": "string - Item description", - "category": "string - Item category", - "unit": "string - Unit of measurement", - "price": "float - Item price", - "stock": "float - Current stock quantity", - "minStock": "float - Minimum stock level", - "maxStock": "float - Maximum stock level", - "supplier": "string - Supplier name", - "barcode": "string - Barcode", - "isActive": "boolean - Item active status" - }, - "examples": { - "input": {"code": "1001"}, - "output": { - "id": 45, - "code": "1001", - "name": "لپ‌تاپ اپل", - "description": "لپ‌تاپ اپل مک‌بوک پرو 13 اینچ", - "category": "الکترونیک", - "unit": "عدد", - "price": 45000000, - "stock": 15, - "minStock": 5, - "maxStock": 50, - "supplier": "شرکت اپل", - "barcode": "1234567890123", - "isActive": true - } - } - }'; + "tool": "addOrUpdateCommodity", + "description": "برای ویرایش یک کالا ابتدا باید با ابزار جست‌وجوی کالا (در آینده) کالا را پیدا کنید. اگر چند نتیجه یافت شد، باید از کاربر بپرسید کدام را می‌خواهد ویرایش کند و کد (code) آن را دریافت کنید. سپس با ارسال کد و اطلاعات جدید به این ابزار، ویرایش انجام می‌شود. اگر code برابر 0 یا ارسال نشود، کالا/خدمت جدید ایجاد خواهد شد. Add a new commodity or update an existing one. If code is 0 or not set, a new commodity will be created. Otherwise, the commodity with the given code will be updated.", + "endpoint": "/api/commodity/mod/{code}", + "method": "POST", + "parameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Commodity name (required)"}, + "priceSell": {"type": "string", "description": "Sell price"}, + "priceBuy": {"type": "string", "description": "Buy price"}, + "des": {"type": "string", "description": "Description"}, + "unit": {"type": "string", "description": "Unit name (required)"}, + "code": {"type": ["integer", "string"], "description": "Commodity code (0 for new, otherwise for update)"}, + "khadamat": {"type": "boolean", "description": "Is service?"}, + "cat": {"type": "object", "description": "Category object (id, code, name, ...)"}, + "orderPoint": {"type": "integer", "description": "Order point"}, + "commodityCountCheck": {"type": "boolean", "description": "Count check flag"}, + "minOrderCount": {"type": "integer", "description": "Minimum order count"}, + "dayLoading": {"type": "integer", "description": "Day loading"}, + "speedAccess": {"type": "boolean", "description": "Quick access flag"}, + "withoutTax": {"type": "boolean", "description": "Without tax flag"}, + "barcodes": {"type": "string", "description": "Barcodes"}, + "prices": {"type": "array", "items": {"type": "object"}, "description": "Prices list"}, + "taxCode": {"type": "string", "description": "Tax code"}, + "taxType": {"type": "string", "description": "Tax type"}, + "taxUnit": {"type": "string", "description": "Tax unit"}, + "acc": {"type": "object", "description": "Access info (required for backend)"} + }, + "required": ["name", "unit"] + }, + "output": { + "Success": "boolean", + "result": "integer", + "code": "integer" + }, + "examples": { + "input": {"name":"میخ","priceSell":"6500","priceBuy":"5500","des":"","unit":"عدد","code":0,"khadamat":false,"cat":{"id":4,"code":4,"name":"بدون دسته‌بندی","checked":false,"root":null,"upper":"3"},"orderPoint":0,"commodityCountCheck":false,"minOrderCount":1,"dayLoading":0,"speedAccess":false,"withoutTax":false,"barcodes":"","prices":[],"taxCode":"","taxType":"","taxUnit":"","acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}}, + "output": {"Success":true,"result":1,"code":2} + } +}'; } } \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php b/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php index c0a6645..518c4b5 100644 --- a/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php +++ b/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php @@ -47,6 +47,33 @@ class PersonPromptService ]; } + // ابزار getPersonsList + $personListPrompt = $this->getPersonsListPrompt(); + $personListData = json_decode($personListPrompt, true); + if ($personListData) { + $tools[] = [ + 'type' => 'function', + 'function' => [ + 'name' => $personListData['tool'], + 'description' => $personListData['description'], + 'parameters' => $personListData['parameters'] + ] + ]; + } + // ابزار addOrUpdatePerson + $addOrUpdatePrompt = $this->getAddOrUpdatePersonPrompt(); + $addOrUpdateData = json_decode($addOrUpdatePrompt, true); + if ($addOrUpdateData) { + $tools[] = [ + 'type' => 'function', + 'function' => [ + 'name' => $addOrUpdateData['tool'], + 'description' => $addOrUpdateData['description'], + 'parameters' => $addOrUpdateData['parameters'] + ] + ]; + } + return $tools; } @@ -57,17 +84,9 @@ class PersonPromptService public function getAllPersonPrompts(): string { $prompts = []; - - // اضافه کردن تمام پرامپ‌های موجود $prompts[] = $this->getPersonInfoPrompt(); - - // در آینده پرامپ‌های دیگر اضافه خواهند شد - // $prompts[] = $this->getCreatePersonPrompt(); - // $prompts[] = $this->getUpdatePersonPrompt(); - // $prompts[] = $this->getSearchPersonPrompt(); - // $prompts[] = $this->getDeletePersonPrompt(); - - // ترکیب تمام پرامپ‌ها + $prompts[] = $this->getPersonsListPrompt(); + $prompts[] = $this->getAddOrUpdatePersonPrompt(); return implode("\n\n", $prompts); } @@ -189,4 +208,164 @@ class PersonPromptService }'; } + public function getPersonsListPrompt(): string + { + return '{ + "tool": "getPersonsList", + "description": "Search and list persons with filters, pagination, and types. The parameters types (person type: e.g., customer, marketer, etc.) and transactionFilters (debtors, creditors, zero) are optional and help to narrow down the search. Normally, you do not need to send these parameters unless you want a more precise search.", + "endpoint": "/api/person/list", + "method": "POST", + "parameters": { + "type": "object", + "properties": { + "page": {"type": "integer", "description": "Page number"}, + "itemsPerPage": {"type": "integer", "description": "Number of items per page"}, + "search": {"type": "string", "description": "Search text (name, code, mobile, etc.)"}, + "types": {"type": "array", "items": {"type": "string"}, "description": "Person types (e.g., customer, marketer, etc.) - optional"}, + "transactionFilters": {"type": "array", "items": {"type": "string"}, "description": "Transaction filters (debtors, creditors, zero) - optional"}, + "sortBy": {"type": ["string", "null"], "description": "Sort field (optional)"}, + "acc": {"type": "object", "description": "Access info (required for backend)"} + }, + "required": ["page", "itemsPerPage", "search"] + }, + "output": { + "items": [ + { + "id": "integer", + "code": "string", + "nikename": "string", + "name": "string", + "tel": "string", + "mobile": "string", + "mobile2": "string", + "des": "string", + "company": "string", + "shenasemeli": "string", + "sabt": "string", + "shahr": "string", + "keshvar": "string", + "ostan": "string", + "postalcode": "string", + "codeeghtesadi": "string", + "email": "string", + "website": "string", + "fax": "string", + "birthday": "string|null", + "speedAccess": "boolean", + "address": "string", + "prelabel": "string|null", + "accounts": "array", + "types": "array", + "bs": "float", + "bd": "float", + "balance": "float" + } + ], + "total": "integer", + "unfilteredTotal": "integer" + }, + "examples": { + "input": {"page":1,"itemsPerPage":10,"search":"بابک","types":["customer","marketer","emplyee","supplier","colleague","salesman"],"transactionFilters":["debtors","creditors"],"sortBy":null,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}}, + "output": { + "items": [ + { + "id": 2, + "code": "1000", + "nikename": "بابک علی زاده", + "name": "", + "tel": "", + "mobile": "", + "mobile2": "", + "des": "", + "company": "", + "shenasemeli": "", + "sabt": "", + "shahr": "", + "keshvar": "", + "ostan": "", + "postalcode": "6761656589", + "codeeghtesadi": "", + "email": "", + "website": "", + "fax": "", + "birthday": null, + "speedAccess": false, + "address": "", + "prelabel": "آقای", + "accounts": [ + { + "bank": "صادرات", + "shabaNum": "125210000000032563214444", + "cardNum": "6037998121456321", + "accountNum": "123456" + } + ], + "types": [ + {"label": "مشتری", "code": "customer", "checked": false}, + {"label": "بازاریاب", "code": "marketer", "checked": false}, + {"label": "کارمند", "code": "emplyee", "checked": true}, + {"label": "تامین‌کننده", "code": "supplier", "checked": true}, + {"label": "همکار", "code": "colleague", "checked": true}, + {"label": "فروشنده", "code": "salesman", "checked": true} + ], + "bs": 0, + "bd": 0, + "balance": 0 + } + ], + "total": 1, + "unfilteredTotal": 4 + } + } +}'; + } + + public function getAddOrUpdatePersonPrompt(): string + { + return '{ + "tool": "addOrUpdatePerson", + "description": "برای ویرایش یک شخص ابتدا باید با ابزار جست‌وجوی شخص (getPersonsList) شخص مورد نظر را پیدا کنید. اگر چند نتیجه یافت شد، باید از کاربر بپرسید کدام را می‌خواهد ویرایش کند و کد (code) آن را دریافت کنید. سپس با ارسال کد و اطلاعات جدید به این ابزار، ویرایش انجام می‌شود. اگر code برابر 0 یا ارسال نشود، شخص جدید ایجاد خواهد شد. Add a new person or update an existing person. If code is 0 or not set, a new person will be created. Otherwise, the person with the given code will be updated.", + "endpoint": "/api/person/mod/{code}", + "method": "POST", + "parameters": { + "type": "object", + "properties": { + "nikename": {"type": "string", "description": "Person nickname (required)"}, + "name": {"type": "string", "description": "Person name"}, + "des": {"type": "string", "description": "Description"}, + "tel": {"type": "string", "description": "Telephone number"}, + "mobile": {"type": "string", "description": "Mobile number"}, + "mobile2": {"type": "string", "description": "Secondary mobile"}, + "address": {"type": "string", "description": "Address"}, + "company": {"type": "string", "description": "Company name"}, + "shenasemeli": {"type": "string", "description": "National ID"}, + "codeeghtesadi": {"type": "string", "description": "Economic code"}, + "sabt": {"type": "string", "description": "Registration number"}, + "keshvar": {"type": "string", "description": "Country"}, + "ostan": {"type": "string", "description": "Province"}, + "shahr": {"type": "string", "description": "City"}, + "postalcode": {"type": "string", "description": "Postal code"}, + "email": {"type": "string", "description": "Email address"}, + "website": {"type": "string", "description": "Website"}, + "fax": {"type": "string", "description": "Fax number"}, + "code": {"type": ["integer", "string"], "description": "Person code (0 for new, otherwise for update)"}, + "types": {"type": "array", "items": {"type": "object"}, "description": "Person types (array of {label, code, checked})"}, + "accounts": {"type": "array", "items": {"type": "object"}, "description": "Bank accounts (array of {bank, accountNum, cardNum, shabaNum})"}, + "prelabel": {"type": "string", "description": "Pre label (e.g., آقای, دکتر, etc.)"}, + "speedAccess": {"type": "boolean", "description": "Quick access flag"}, + "birthday": {"type": "string", "description": "Birthday"}, + "acc": {"type": "object", "description": "Access info (required for backend)"} + }, + "required": ["nikename"] + }, + "output": { + "Success": "boolean", + "result": "integer" + }, + "examples": { + "input": {"nikename":"بهتاش","name":"بهتاش عابدینی","des":"توضیحات","tel":"","mobile":"09183282405","mobile2":"","address":"","company":"آذرخش","shenasemeli":"123848","codeeghtesadi":"4864864","sabt":"468468","keshvar":"ایران","ostan":"کرمانشاه","shahr":"اسلام آباد غرب","postalcode":"6761656589","email":"","website":"","fax":"","code":0,"types":[{"label":"مشتری","code":"customer","checked":false},{"label":"بازاریاب","code":"marketer","checked":false},{"label":"کارمند","code":"emplyee","checked":false},{"label":"تامین‌کننده","code":"supplier","checked":false},{"label":"همکار","code":"colleague","checked":false},{"label":"فروشنده","code":"salesman","checked":false}],"accounts":[{"bank":"ملی","accountNum":"123456","cardNum":"12888787","shabaNum":"8484220000051515150"}],"prelabel":"دکتر","speedAccess":false,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}}, + "output": {"Success":true,"result":1} + } +}'; + } } \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/PromptService.php b/hesabixCore/src/Service/AGI/Promps/PromptService.php index a6117eb..25c042c 100644 --- a/hesabixCore/src/Service/AGI/Promps/PromptService.php +++ b/hesabixCore/src/Service/AGI/Promps/PromptService.php @@ -5,6 +5,7 @@ namespace App\Service\AGI\Promps; use Doctrine\ORM\EntityManagerInterface; use App\Service\AGI\Promps\InventoryPromptService; use App\Service\AGI\Promps\BankPromptService; +use App\Service\AGI\Promps\AccountingDocPromptService; class PromptService { @@ -13,19 +14,22 @@ class PromptService private $basePromptService; private $inventoryPromptService; private $bankPromptService; + private $accountingDocPromptService; public function __construct( EntityManagerInterface $entityManager, PersonPromptService $personPromptService, BasePromptService $basePromptService, InventoryPromptService $inventoryPromptService, - BankPromptService $bankPromptService + BankPromptService $bankPromptService, + AccountingDocPromptService $accountingDocPromptService ) { $this->em = $entityManager; $this->personPromptService = $personPromptService; $this->basePromptService = $basePromptService; $this->inventoryPromptService = $inventoryPromptService; $this->bankPromptService = $bankPromptService; + $this->accountingDocPromptService = $accountingDocPromptService; } /** @@ -48,9 +52,9 @@ class PromptService $bankTools = $this->bankPromptService->getTools(); $tools = array_merge($tools, $bankTools); - // در آینده ابزارهای بخش‌های دیگر اضافه خواهند شد - // $accountingTools = $this->accountingPromptService->getTools(); - // $tools = array_merge($tools, $accountingTools); + // ابزارهای بخش اسناد حسابداری + $accountingTools = $this->accountingDocPromptService->getTools(); + $tools = array_merge($tools, $accountingTools); return $tools; } @@ -107,7 +111,10 @@ class PromptService // پرامپ‌های بخش بانک‌ها $prompts['bank'] = $this->bankPromptService->getAllBankPrompts(); - + + // پرامپ‌های بخش اسناد حسابداری + $prompts['accounting'] = $this->accountingDocPromptService->getAllAccountingDocPrompts(); + // در آینده بخش‌های دیگر اضافه خواهند شد // $prompts['accounting'] = $this->accountingPromptService->getAllAccountingPrompts(); // $prompts['reports'] = $this->reportsPromptService->getAllReportsPrompts(); diff --git a/webUI/src/views/user/manager/settings/system.vue b/webUI/src/views/user/manager/settings/system.vue index 85ab44e..ef5efbe 100755 --- a/webUI/src/views/user/manager/settings/system.vue +++ b/webUI/src/views/user/manager/settings/system.vue @@ -73,6 +73,7 @@ export default defineComponent({ inputTokenPrice: 0, outputTokenPrice: 0, aiPrompt: '', + aiDebugMode: false, aiAgentSources: [ { title: 'GapGPT', value: 'gapgpt', subtitle: 'gapgpt.app' }, { title: 'AvalAI', value: 'avalai', subtitle: 'avalai.ir' }, @@ -164,6 +165,7 @@ export default defineComponent({ this.inputTokenPrice = parseFloat(data.inputTokenPrice) || 0; this.outputTokenPrice = parseFloat(data.outputTokenPrice) || 0; this.aiPrompt = data.aiPrompt || ''; + this.aiDebugMode = data.aiDebugMode === '1' || data.aiDebugMode === true; this.loading = false; }) }, @@ -224,7 +226,8 @@ export default defineComponent({ localModelAddress: this.localModelAddress, inputTokenPrice: this.inputTokenPrice, outputTokenPrice: this.outputTokenPrice, - aiPrompt: this.aiPrompt + aiPrompt: this.aiPrompt, + aiDebugMode: this.aiDebugMode }; axios.post('/api/admin/settings/system/info/save', submitData).then((resp) => { @@ -876,6 +879,19 @@ export default defineComponent({ mdi-information این متن قبل از هر سوال به هوش مصنوعی ارسال می‌شود تا رفتار و پاسخ‌دهی آن را کنترل کند + +
+ mdi-information + اگر این گزینه فعال باشد، اطلاعات دیباگ (debug_info) در خروجی پاسخ‌های هوش مصنوعی نمایش داده می‌شود. +
diff --git a/webUI/src/views/wizard/home.vue b/webUI/src/views/wizard/home.vue index e399bab..6bc0f62 100755 --- a/webUI/src/views/wizard/home.vue +++ b/webUI/src/views/wizard/home.vue @@ -1,6 +1,105 @@ @@ -118,6 +235,7 @@ import { useNavigationStore } from '@/stores/navigationStore'; import axios from 'axios'; import AIChart from '@/components/widgets/AIChart.vue'; +import { h } from 'vue'; export default { name: "WizardHome", @@ -137,12 +255,23 @@ export default { aiEnabled: false, aiStatus: 'checking', conversationId: null, + showConversations: false, + conversations: [], quickSuggestions: [ - 'چطور می‌تونم کمکتون کنم؟', - 'سوالی دارید؟', - 'نیاز به راهنمایی دارید؟', - 'مشکلی پیش اومده؟' - ] + 'چگونه می‌توانم یک گزارش مالی تهیه کنم؟', + 'برای ثبت یک فاکتور جدید چه مراحلی را باید طی کنم؟', + 'چطور می‌توانم کاربران جدید به سیستم اضافه کنم؟', + 'چگونه می‌توانم تنظیمات کسب‌وکارم را تغییر دهم؟' + ], + showDeleteDialog: false, + deleteTargetId: null, + loadingConversation: false, + loadingDelete: false, + showSnackbar: false, + snackbarText: '', + snackbarColor: 'success', + showDeleteAllDialog: false, + loadingDeleteAll: false, } }, methods: { @@ -350,7 +479,205 @@ export default { } }, 100); }); - } + }, + + async fetchConversations() { + const res = await axios.post('/api/wizard/conversations/list'); + if (res.data.success) { + this.conversations = res.data.items; + } + }, + async createConversation() { + this.loadingConversation = true; + const res = await axios.post('/api/wizard/conversations/create', { title: 'گفتگوی جدید' }); + if (res.data.success) { + this.showConversations = false; + this.conversationId = res.data.id; + this.messages = [ + { + type: 'ai', + data: { type: ['text'], data: [{ type: 'text', content: 'سلام! گفت‌وگوی جدید ایجاد شد. پیام خود را بنویسید.' }] }, + timestamp: new Date() + } + ]; + await this.fetchConversations(); + } + this.loadingConversation = false; + }, + async switchConversation(id) { + this.loadingConversation = true; + this.conversationId = id; + this.showConversations = false; + // دریافت پیام‌های گفتگو + const res = await axios.post(`/api/wizard/conversations/${id}/messages`); + if (res.data.success) { + this.messages = res.data.items.map(msg => { + const role = (msg.role || '').toLowerCase(); + if (role.includes('user')) { + return { + type: 'user', + text: msg.content, + timestamp: new Date(msg.createdAt) + }; + } else if (role.includes('ai') || role.includes('assistant') || role.includes('system')) { + let parsed = null; + try { + parsed = msg.content; + let safety = 0; + while (typeof parsed === 'string' && safety < 5) { + parsed = JSON.parse(parsed); + safety++; + } + if ( + parsed && + parsed.data && + Array.isArray(parsed.data) && + typeof parsed.data[0] === 'string' + ) { + let safety2 = 0; + while (typeof parsed.data[0] === 'string' && safety2 < 5) { + parsed.data[0] = JSON.parse(parsed.data[0]); + safety2++; + } + } + } catch (e) { + parsed = { type: ['text'], data: [{ type: 'text', content: msg.content }] }; + } + return { + type: 'ai', + data: parsed, + timestamp: new Date(msg.createdAt) + }; + } else { + // پیش‌فرض: پیام کاربر + return { + type: 'user', + text: msg.content, + timestamp: new Date(msg.createdAt) + }; + } + }); + } + this.loadingConversation = false; + }, + async deleteConversation(id) { + this.loadingConversation = true; + await axios.post(`/api/wizard/conversations/${id}/delete`); + await this.fetchConversations(); + if (this.conversationId === id) { + this.conversationId = null; + this.messages = [ + { + type: 'ai', + data: { type: ['text'], data: [{ type: 'text', content: 'گفت‌وگو حذف شد. یک گفت‌وگوی جدید شروع کنید.' }] }, + timestamp: new Date() + } + ]; + } + this.loadingConversation = false; + }, + confirmDelete(id) { + this.deleteTargetId = id; + this.showDeleteDialog = true; + }, + async doDeleteConversation() { + if (this.deleteTargetId) { + this.loadingDelete = true; + try { + await this.deleteConversation(this.deleteTargetId); + this.showDeleteDialog = false; + this.deleteTargetId = null; + this.snackbarText = 'گفت‌وگو با موفقیت حذف شد.'; + this.snackbarColor = 'success'; + this.showSnackbar = true; + } catch (e) { + this.snackbarText = 'خطا در حذف گفت‌وگو!'; + this.snackbarColor = 'error'; + this.showSnackbar = true; + } + this.loadingDelete = false; + } + }, + async deleteAllConversations() { + this.loadingDeleteAll = true; + try { + const res = await axios.post('/api/wizard/conversations/delete-all'); + if (res.data.success) { + this.showDeleteAllDialog = false; + this.snackbarText = 'همه گفت‌وگوها با موفقیت حذف شدند.'; + this.snackbarColor = 'success'; + this.showSnackbar = true; + await this.fetchConversations(); + this.conversationId = null; + this.messages = [ + { + type: 'ai', + data: { type: ['text'], data: [{ type: 'text', content: 'همه گفت‌وگوها حذف شدند. یک گفت‌وگوی جدید شروع کنید.' }] }, + timestamp: new Date() + } + ]; + } else { + this.snackbarText = 'خطا در حذف همه گفت‌وگوها!'; + this.snackbarColor = 'error'; + this.showSnackbar = true; + } + } catch (e) { + this.snackbarText = 'خطا در حذف همه گفت‌وگوها!'; + this.snackbarColor = 'error'; + this.showSnackbar = true; + } + this.loadingDeleteAll = false; + }, + getAvatarColor(title) { + // تولید رنگ ثابت بر اساس عنوان گفتگو + const colors = ['primary', 'deep-purple', 'indigo', 'teal', 'cyan', 'pink', 'orange', 'green', 'blue', 'red']; + let hash = 0; + for (let i = 0; i < title.length; i++) hash = title.charCodeAt(i) + ((hash << 5) - hash); + return colors[Math.abs(hash) % colors.length]; + }, + renderLastMessagePreview(msg) { + if (!msg || typeof msg !== 'string') return { + render() { return h('span', msg || 'بدون پیام'); } + }; + let parsed; + try { + parsed = JSON.parse(msg); + } catch (e) { + return { + render() { return h('span', msg); } + }; + } + if (parsed && parsed.type && parsed.data && Array.isArray(parsed.data)) { + if (parsed.type.includes('text')) { + const textItem = parsed.data.find(d => d.type === 'text'); + if (textItem && textItem.content) { + return { + render() { return h('span', textItem.content); } + }; + } + } + if (parsed.type.includes('table')) { + const tableItem = parsed.data.find(d => d.type === 'table'); + if (tableItem && tableItem.headers && tableItem.rows) { + return { + render() { + return h('v-simple-table', { class: 'conv-preview-table' }, [ + h('thead', [ + h('tr', tableItem.headers.map(hd => h('th', hd))) + ]), + h('tbody', tableItem.rows.map(row => + h('tr', row.map(cell => h('td', cell))) + )) + ]); + } + }; + } + } + } + return { + render() { return h('span', msg); } + }; + }, }, async mounted() { @@ -362,6 +689,7 @@ export default { // بررسی وضعیت هوش مصنوعی await this.checkAIStatus(); + await this.fetchConversations(); this.scrollToBottom(); }, @@ -602,4 +930,185 @@ export default { max-width: 90%; } } + +.conversation-item { + cursor: pointer; + border-radius: 12px; + transition: background 0.2s; +} +.conversation-item:hover { + background: #f5f5f5; +} +.conv-dialog-card { + border-radius: 20px; + box-shadow: 0 8px 32px rgba(25, 118, 210, 0.10); + background: linear-gradient(135deg, #f8fafc 0%, #e3eafc 100%); +} +.conv-dialog-title { + background: linear-gradient(90deg, #1976d2 0%, #42a5f5 100%); + color: white; + border-top-left-radius: 20px; + border-top-right-radius: 20px; + min-height: 56px; +} +.conv-new-btn { + border-radius: 12px; + font-weight: bold; + font-size: 15px; + box-shadow: 0 2px 8px rgba(25, 118, 210, 0.08); +} +.conv-list { + background: transparent; +} +.conv-list-item { + margin-bottom: 6px; + border-radius: 14px; + transition: background 0.2s, box-shadow 0.2s; + box-shadow: 0 1px 4px rgba(25, 118, 210, 0.04); + border: 1px solid #e3eafc; +} +.conv-list-item:hover, .active-conv { + background: linear-gradient(90deg, #e3f2fd 0%, #f5faff 100%); + box-shadow: 0 4px 16px rgba(25, 118, 210, 0.10); + border-color: #90caf9; +} +.empty-state { + opacity: 0.7; + font-size: 15px; +} +.conv-dialog-card-new { + border-radius: 24px; + box-shadow: 0 12px 40px rgba(25, 118, 210, 0.13); + background: linear-gradient(135deg, #fafdff 0%, #e3eafc 100%); + overflow: hidden; +} +.conv-dialog-header-new { + background: linear-gradient(90deg, #1976d2 0%, #42a5f5 100%); + color: white; + border-top-left-radius: 24px; + border-top-right-radius: 24px; + min-height: 64px; + font-size: 20px; + letter-spacing: 0.5px; + box-shadow: 0 2px 8px rgba(25, 118, 210, 0.08); +} +.conv-dialog-body-new { + padding: 18px 0 0 0; + min-height: 320px; + max-height: 60vh; + overflow-y: auto; +} +.conv-card-item-new { + display: flex; + align-items: center; + background: linear-gradient(90deg, #f5faff 0%, #e3f2fd 100%); + border-radius: 18px; + box-shadow: 0 2px 12px rgba(25, 118, 210, 0.07); + margin-bottom: 14px; + padding: 12px 18px 12px 8px; + transition: box-shadow 0.2s, background 0.2s; + border: 1.5px solid #e3eafc; + cursor: pointer; + position: relative; +} +.conv-card-item-new.active { + background: linear-gradient(90deg, #e3f2fd 0%, #bbdefb 100%); + border-color: #90caf9; + box-shadow: 0 6px 24px rgba(25, 118, 210, 0.13); +} +.conv-card-main { + display: flex; + align-items: center; + flex: 1; + min-width: 0; +} +.conv-title-row { + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + font-weight: 600; +} +.conv-title { + color: #1976d2; + font-weight: bold; + font-size: 16px; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.conv-count { + color: #42a5f5; + font-size: 13px; + font-weight: 500; + display: flex; + align-items: center; + gap: 2px; +} +.conv-last-message { + color: #607d8b; + font-size: 13px; + margin-top: 2px; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.conv-time { + color: #90a4ae; + font-size: 12px; + margin-right: 12px; + min-width: 70px; + text-align: left; +} +.conv-delete-btn { + margin-right: 8px; + margin-left: 0; + z-index: 2; +} +.conv-dialog-actions-new { + padding: 18px 24px 18px 24px; + background: transparent; + border-bottom-left-radius: 24px; + border-bottom-right-radius: 24px; + box-shadow: 0 -2px 8px rgba(25, 118, 210, 0.04); +} +.conv-new-btn-new { + border-radius: 14px; + font-weight: bold; + font-size: 16px; + box-shadow: 0 2px 8px rgba(25, 118, 210, 0.10); + padding: 14px 0; +} +.conv-empty-state-new { + opacity: 0.8; + font-size: 16px; + text-align: center; + margin-top: 48px; +} +.conv-list-fade-enter-active, .conv-list-fade-leave-active { + transition: all 0.3s cubic-bezier(.4,0,.2,1); +} +.conv-list-fade-enter-from, .conv-list-fade-leave-to { + opacity: 0; + transform: translateY(20px); +} +.conv-list-item-v { + border-radius: 14px; + margin-bottom: 4px; + transition: background 0.2s, box-shadow 0.2s; + cursor: pointer; +} +.conv-list-item-v:hover, .conv-list-item-v.v-list-item--active { + background: linear-gradient(90deg, #e3f2fd 0%, #f5faff 100%); + box-shadow: 0 4px 16px rgba(25, 118, 210, 0.10); +} +.conv-preview-table { + font-size: 12px; + margin-top: 2px; + background: #f8fafc; + border-radius: 6px; + overflow: hidden; +}