more progress in agi and support external tools

This commit is contained in:
Hesabix 2025-07-24 19:19:53 +00:00
parent 474f1274c0
commit bad8dc0f73
20 changed files with 1837 additions and 745 deletions

View file

@ -97,6 +97,32 @@ services:
tags: ['twig.extension'] tags: ['twig.extension']
App\Cog\PersonService: 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: arguments:
$entityManager: '@doctrine.orm.entity_manager' $entityManager: '@doctrine.orm.entity_manager'
$access: '@App\Service\Access' $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'

View file

@ -0,0 +1,37 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Cog\AccountingDocService as CogAccountingDocService;
class AccountingDocService
{
private EntityManagerInterface $em;
private CogAccountingDocService $cogAccountingDocService;
public function __construct(EntityManagerInterface $em, CogAccountingDocService $cogAccountingDocService)
{
$this->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()
];
}
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Cog\CommodityService as CogCommodityService;
class CommodityService
{
private EntityManagerInterface $em;
private CogCommodityService $cogCommodityService;
public function __construct(EntityManagerInterface $em, CogCommodityService $cogCommodityService)
{
$this->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()
];
}
}
}

View file

@ -32,6 +32,7 @@ class PersonService
]; ];
} }
try { try {
// فقط کد را به سرویس Cog پاس بده
return $this->cogPersonService->getPersonInfo($code, $acc); return $this->cogPersonService->getPersonInfo($code, $acc);
} catch (\Exception $e) { } catch (\Exception $e) {
return [ 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()
];
}
}
} }

View file

@ -0,0 +1,116 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
class AccountingDocService
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->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;
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Commodity;
use App\Entity\CommodityUnit;
use App\Entity\CommodityCat;
use App\Entity\PriceList;
use App\Entity\PriceListDetail;
class CommodityService
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->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()];
}
}

View file

@ -16,15 +16,13 @@ use App\Service\Explore;
class PersonService class PersonService
{ {
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
private array $access;
/** /**
* سازنده سرویس * سازنده سرویس
*/ */
public function __construct(EntityManagerInterface $entityManager, array $access) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
$this->access = $access;
} }
/** /**
@ -74,4 +72,212 @@ class PersonService
return $response; 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];
}
} }

View file

@ -469,6 +469,7 @@ class AdminController extends AbstractController
$resp['inputTokenPrice'] = $registryMGR->get('system', key: 'inputTokenPrice'); $resp['inputTokenPrice'] = $registryMGR->get('system', key: 'inputTokenPrice');
$resp['outputTokenPrice'] = $registryMGR->get('system', key: 'outputTokenPrice'); $resp['outputTokenPrice'] = $registryMGR->get('system', key: 'outputTokenPrice');
$resp['aiPrompt'] = $registryMGR->get('system', key: 'aiPrompt'); $resp['aiPrompt'] = $registryMGR->get('system', key: 'aiPrompt');
$resp['aiDebugMode'] = $registryMGR->get('system', key: 'aiDebugMode');
return $this->json($resp); return $this->json($resp);
} }
@ -521,6 +522,8 @@ class AdminController extends AbstractController
$registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice'] ?? ''); $registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice'] ?? '');
if (array_key_exists('aiPrompt', $params)) if (array_key_exists('aiPrompt', $params))
$registryMGR->update('system', 'aiPrompt', $params['aiPrompt'] ?? ''); $registryMGR->update('system', 'aiPrompt', $params['aiPrompt'] ?? '');
if (array_key_exists('aiDebugMode', $params))
$registryMGR->update('system', 'aiDebugMode', $params['aiDebugMode'] ?? '');
$entityManager->persist($item); $entityManager->persist($item);
$entityManager->flush(); $entityManager->flush();

View file

@ -823,138 +823,13 @@ class CommodityController extends AbstractController
if ($content = $request->getContent()) { if ($content = $request->getContent()) {
$params = json_decode($content, true); $params = json_decode($content, true);
} }
if (!array_key_exists('name', $params)) $commodityService = new \App\Cog\CommodityService($entityManager);
return $this->json(['result' => -1]); $result = $commodityService->addOrUpdateCommodity($params, $acc, $code);
if (count_chars(trim($params['name'])) == 0) if (isset($result['error'])) {
return $this->json(['result' => 3]); return $this->json($result, 400);
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();
}
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')); $log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
return $this->json([ return $this->json($result);
'Success' => true,
'result' => 1,
'code' => $data->getId()
]);
} }
#[Route('/api/commodity/units', name: 'app_commodity_units')] #[Route('/api/commodity/units', name: 'app_commodity_units')]

View file

@ -848,99 +848,15 @@ class HesabdariController extends AbstractController
if ($content = $request->getContent()) { if ($content = $request->getContent()) {
$params = json_decode($content, true); $params = json_decode($content, true);
} }
if (!array_key_exists('type', $params)) $acc = $access->hasRole($params['type'] ?? 'accounting');
$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);
if (!$acc) if (!$acc)
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
if ($params['type'] == 'person') { $service = new \App\Cog\AccountingDocService($entityManager);
$person = $entityManager->getRepository(Person::class)->findOneBy([ $result = $service->searchRows($params, $acc);
'bid' => $acc['bid'], if (isset($result['error'])) {
'code' => $params['id'], return $this->json($result, 400);
]);
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'
]);
} }
$dataTemp = []; return $this->json($result);
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);
} }
#[Route('/api/accounting/table/get', name: 'app_accounting_table_get')] #[Route('/api/accounting/table/get', name: 'app_accounting_table_get')]

View file

@ -238,155 +238,36 @@ class PersonsController extends AbstractController
if ($content = $request->getContent()) { if ($content = $request->getContent()) {
$params = json_decode($content, true); $params = json_decode($content, true);
} }
if (!array_key_exists('nikename', $params)) $personService = new \App\Cog\PersonService($entityManager);
return $this->json(['result' => -1]); $result = $personService->addOrUpdatePerson($params, $acc, $code);
if (count_chars(trim($params['nikename'])) == 0) if (isset($result['error'])) {
return $this->json(['result' => 3]); return $this->json($result, 400);
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();
}
$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']); $log->insert('اشخاص', 'شخص با نام مستعار ' . $params['nikename'] . ' افزوده/ویرایش شد.', $this->getUser(), $acc['bid']);
return $this->json([ return $this->json($result);
'Success' => true, }
'result' => 1,
]); #[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')] #[Route('/api/person/list/search', name: 'app_persons_list_search')]
@ -478,115 +359,6 @@ class PersonsController extends AbstractController
return $this->json($response); 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')] #[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 public function app_persons_list_debtors(string $amount, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{ {

View file

@ -29,7 +29,8 @@ class wizardController extends AbstractController
Request $request, Request $request,
Access $access, Access $access,
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
Log $log Log $log,
\App\Service\registryMGR $registryMGR
): JsonResponse ): JsonResponse
{ {
@ -73,6 +74,53 @@ class wizardController extends AbstractController
$options = $params['options'] ?? []; $options = $params['options'] ?? [];
$conversationId = $params['conversationId'] ?? null; $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(); $aiStatus = $this->agiService->checkAIServiceStatus();
if (!$aiStatus['isEnabled']) { if (!$aiStatus['isEnabled']) {
@ -110,19 +158,31 @@ class wizardController extends AbstractController
// استفاده از AGIService برای مدیریت گفتگو و ارسال درخواست // استفاده از AGIService برای مدیریت گفتگو و ارسال درخواست
$result = $this->agiService->sendRequest($message, $business, $acc['user'], $conversationId, $acc); $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']) { if ($result['success']) {
$responseContent = $result['response'] ?? $result['message'] ?? 'عملیات با موفقیت انجام شد'; $responseContent = $result['response'] ?? $result['message'] ?? 'عملیات با موفقیت انجام شد';
$response = [ $response = [
'success' => true, 'success' => true,
'response' => $responseContent, 'response' => $responseContent,
'conversationId' => $result['conversation_id'] ?? null, 'conversationId' => $result['conversationId'] ?? $result['conversation_id'] ?? null,
'model' => $result['model'] ?? null, 'model' => $result['model'] ?? null,
'usage' => $result['usage'] ?? null, 'usage' => $result['usage'] ?? null,
'cost' => $result['cost'] ?? null, 'cost' => $result['cost'] ?? null,
'debug_info' => $result['debug_info'] ?? null 'debug_info' => $result['debug_info'] ?? null
]; ];
// اگر دیباگ خاموش بود، debug_info و model را حذف کن
if (!$aiDebugMode) {
unset($response['debug_info']);
unset($response['model']);
}
// محاسبه هزینه در صورت وجود اطلاعات usage // محاسبه هزینه در صورت وجود اطلاعات usage
if (isset($result['cost'])) { if (isset($result['cost'])) {
$cost = $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]);
}
} }

View file

@ -102,6 +102,11 @@ class AGIService
$result = $this->sendToAIServiceWithFunctionCalling($userPrompt, $apiKey, $service, $conversationHistory, $acc); $result = $this->sendToAIServiceWithFunctionCalling($userPrompt, $apiKey, $service, $conversationHistory, $acc);
if (!$result['success']) { 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; return $result;
} }
@ -112,18 +117,24 @@ class AGIService
// ذخیره پاسخ هوش مصنوعی // ذخیره پاسخ هوش مصنوعی
$this->saveAIMessage($conversation, $aiResponse, $result['data'], $cost); $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 [ return [
'success' => true, 'success' => true,
'response' => $aiResponse, 'response' => $aiResponse,
'conversationId' => $conversation->getId(), 'conversationId' => $conversation->getId(), // مقداردهی صحیح conversationId
'model' => $this->getAIModel(), 'model' => $this->getAIModel(),
'usage' => $result['data']['usage'] ?? [], 'usage' => $result['data']['usage'] ?? [],
'cost' => $cost, 'cost' => $cost,
'debug_info' => [ 'debug_info' => $debugInfo
'context' => 'sendRequest',
'function_calls' => $result['function_calls'] ?? [],
'tool_results' => $result['tool_results'] ?? []
]
]; ];
try { try {
} catch (\Exception $e) { } catch (\Exception $e) {
@ -149,24 +160,15 @@ class AGIService
{ {
$urls = $this->getServiceUrls($service); $urls = $this->getServiceUrls($service);
$model = $this->getAIModel(); $model = $this->getAIModel();
// ساخت پرامپت system با buildSmartPrompt
// پیام system شامل قوانین خروجی و مثال دقیق $systemPrompt = $this->buildSmartPrompt($prompt, $acc['bid'] ?? null, $conversationHistory);
$systemPrompt = "شما دستیار هوشمند حسابیکس هستید. فقط پاسخ را به صورت JSON مطابق مثال خروجی بده. اگر نیاز به ابزار داشتی، از function calling استفاده کن."
. $this->promptService->getOutputFormatPrompt();
$messages = [ $messages = [
[ [
'role' => 'system', 'role' => 'system',
'content' => $systemPrompt 'content' => $systemPrompt
] ]
]; ];
// تاریخچه گفتگو // تاریخچه گفتگو و پیام user دیگر اینجا اضافه نمی‌شود چون در buildSmartPrompt لحاظ شده است
foreach ($conversationHistory as $historyItem) {
$messages[] = [
'role' => $historyItem['role'],
'content' => $historyItem['content']
];
}
// پیام user فقط سوال فعلی
$messages[] = [ $messages[] = [
'role' => 'user', 'role' => 'user',
'content' => $prompt 'content' => $prompt
@ -212,7 +214,8 @@ class AGIService
'apiKey' => $apiKey, 'apiKey' => $apiKey,
'service' => $service, 'service' => $service,
'conversationHistory' => $conversationHistory, 'conversationHistory' => $conversationHistory,
'system_prompt' => $systemPrompt,
'messages_to_ai' => $messages,
] ]
]; ];
} }
@ -227,7 +230,9 @@ class AGIService
'debug_info' => [ 'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling', 'context' => 'sendToAIServiceWithFunctionCalling',
'response_data' => $responseData, 'response_data' => $responseData,
'iteration' => $iteration 'iteration' => $iteration,
'system_prompt' => $systemPrompt,
'messages_to_ai' => $messages,
] ]
]; ];
} }
@ -242,7 +247,11 @@ class AGIService
'success' => true, 'success' => true,
'data' => $responseData, 'data' => $responseData,
'function_calls' => $functionCalls, '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', 'context' => 'sendToAIServiceWithFunctionCalling',
'max_iterations' => $maxIterations, 'max_iterations' => $maxIterations,
'function_calls' => $functionCalls, 'function_calls' => $functionCalls,
'tool_results' => $toolResults 'tool_results' => $toolResults,
'system_prompt' => $systemPrompt,
'messages_to_ai' => $messages,
] ]
]; ];
} }
@ -305,10 +316,25 @@ class AGIService
try { try {
switch ($tool) { switch ($tool) {
case 'getPersonInfo': case 'getPersonInfo':
// استفاده مستقیم از سرویس جدید $cogPersonService = new \App\Cog\PersonService($this->em);
$cogPersonService = new \App\Cog\PersonService($this->em, $params['acc'] ?? null);
$personService = new \App\AiTool\PersonService($this->em, $cogPersonService); $personService = new \App\AiTool\PersonService($this->em, $cogPersonService);
return $personService->getPersonInfoByCode($params['code'] ?? null, $params['acc'] ?? null); 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: default:
return [ return [
'error' => 'ابزار ناشناخته: ' . $tool 'error' => 'ابزار ناشناخته: ' . $tool
@ -326,28 +352,41 @@ class AGIService
*/ */
private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string
{ {
// دریافت پرامپ‌های پایه از PromptService // دریافت aiPrompt مدیر سیستم و قوانین خروجی
$basePrompts = $this->promptService->getAllBasePrompts(); $aiPrompt = $this->registryMGR->get('system', 'aiPrompt');
$prompt = $basePrompts; $outputFormatPrompt = $this->promptService->getOutputFormatPrompt();
$basePrompt = "شما دستیار هوشمند حسابیکس هستید. فقط پاسخ را به صورت JSON مطابق مثال خروجی بده. اگر نیاز به ابزار داشتی، از function calling استفاده کن.";
// قوانین خروجی JSON و مثال‌ها از سرویس مدیریت پرامپت‌ها $parts = [];
$prompt .= $this->promptService->getOutputFormatPrompt(); // اضافه کردن 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) { if ($business) {
$prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}."; $parts[] = "اطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}.";
} }
// اضافه کردن تاریخچه گفتگو // اضافه کردن تاریخچه گفتگو
if (!empty($conversationHistory)) { if (!empty($conversationHistory)) {
$prompt .= "\n\n📜 تاریخچه گفتگو:\n"; $historyText = "\n📜 تاریخچه گفتگو:\n";
foreach ($conversationHistory as $historyItem) { foreach ($conversationHistory as $historyItem) {
$role = $historyItem['role'] === 'user' ? 'کاربر' : 'دستیار'; $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) { if ($conversationId) {
// بازیابی گفتگوی موجود // بازیابی گفتگوی موجود
$conversation = $this->em->getRepository(AIConversation::class)->find($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()); $conversation->setUpdatedAt(time());
$this->em->persist($conversation); $this->em->persist($conversation);
return $conversation; return $conversation;
@ -624,6 +680,8 @@ class AGIService
$conversation->setDeleted(false); $conversation->setDeleted(false);
$this->em->persist($conversation); $this->em->persist($conversation);
$this->em->flush(); // اضافه شد تا id مقداردهی شود
return $conversation; return $conversation;
} }
@ -758,7 +816,36 @@ class AGIService
{ {
$conversation = $this->em->getRepository(AIConversation::class)->find($conversationId); $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 []; return [];
} }

View file

@ -0,0 +1,93 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class AccountingDocPromptService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->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": "بابک"
}
]
}
}';
}
}

View file

@ -5,130 +5,35 @@ namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use App\Service\Access; use App\Service\Access;
use App\Entity\APIToken; use App\Entity\APIToken;
use App\Service\registryMGR;
class BasePromptService class BasePromptService
{ {
private $em; private $em;
private $access; private $access;
private $registryMGR;
public function __construct(EntityManagerInterface $entityManager, Access $access) public function __construct(
{ EntityManagerInterface $entityManager,
Access $access,
registryMGR $registryMGR
) {
$this->em = $entityManager; $this->em = $entityManager;
$this->access = $access; $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 * @return string
*/ */
public function getErrorHandlingPrompt(): string public function getErrorHandlingPrompt(): string
{ {
return '{ return '{
"tool": "error_handling", "tool": "reasoning_base",
"description": "Error handling and authentication guidance", "description": "شما یک دستیار هوشمند حسابداری هستید که باید مانند یک فیلسوف و تحلیل‌گر رفتار کنید. هدف شما فقط اجرای مستقیم دستورات نیست، بلکه باید هدف نهایی کاربر را از دل مکالمه و تاریخچه بفهمید و برای رسیدن به آن، ابزار مناسب را انتخاب کنید. ممکن است برای رسیدن به هدف، نیاز باشد ابتدا با یک ابزار (مثلاً جست‌وجوی اشخاص یا کالا) داده‌ای (مثل code یا id) را به دست آورید و سپس آن را به عنوان ورودی ابزار دیگر (مثلاً ویرایش یا جست‌وجوی تراکنش) استفاده کنید. همیشه تحلیل کن که چه داده‌ای نیاز است و چه ابزاری باید فراخوانی شود. در حسابداری، code (کد) و id (شناسه دیتابیس) دو مفهوم کاملاً متفاوت هستند و نباید به جای هم استفاده شوند. اگر کاربر در چند پیام متوالی صحبت کرد (مثلاً ابتدا گفت بابک را پیدا کن و بعد گفت تلفنش را ویرایش کن)، باید از تاریخچه مکالمه بفهمی منظورش تغییر تلفن همان شخص بابک است و ابزار مناسب را با داده صحیح فراخوانی کنی. همیشه سعی کن با تحلیل و استدلال چندمرحله‌ای، بهترین مسیر را برای حل مسئله انتخاب کنی و اگر نیاز به پرسش از کاربر بود، سؤال شفاف و هدفمند بپرس. اگر داده‌ای ناقص بود، با تحلیل تاریخچه یا پرسش از کاربر آن را کامل کن. در تحلیل داده‌های حسابداری، دقت و صحت اطلاعات بسیار مهم است و هرگونه اشتباه در تشخیص ابزار یا داده می‌تواند منجر به خطای مالی شود. خروجی هر ابزار را به دقت بررسی کن و اگر نیاز بود، آن را به عنوان ورودی ابزار بعدی استفاده کن. همیشه به دنبال درک عمیق‌تر هدف کاربر و ارائه راه‌حل بهینه باش.",
"instructions": "When encountering errors, provide clear explanations in Persian and suggest solutions. Focus on authentication and access control issues.", "instructions": "استدلال کن، تحلیل کن، ابزار مناسب را انتخاب کن، از تاریخچه استفاده کن، تفاوت code و id را رعایت کن، خروجی ابزارها را به هم متصل کن و مانند یک متفکر حرفه‌ای عمل کن.",
"error_types": { "response_format": "Explain your reasoning in Persian, select the best tool, and if needed, ask clarifying questions."
"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 . '"
}'; }';
} }
@ -139,12 +44,21 @@ class BasePromptService
public function getAllBasePrompts(): string public function getAllBasePrompts(): string
{ {
$prompts = []; $prompts = [];
$aiPrompt = $this->registryMGR->get('system', 'aiPrompt');
$prompts[] = $this->getSystemIntroductionPrompt(); if ($aiPrompt) {
$prompts[] = $aiPrompt;
}
$prompts[] = $this->getErrorHandlingPrompt(); $prompts[] = $this->getErrorHandlingPrompt();
$prompts[] = $this->getHelpPrompt();
$prompts[] = $this->getApiBaseUrlPrompt();
return implode("\n\n", $prompts); return implode("\n\n", $prompts);
} }
/**
* دریافت تمام ابزارهای پایه
* @return array
*/
public function getAllTools(): array
{
$tools = [];
return $tools;
}
} }

View file

@ -13,111 +13,71 @@ class InventoryPromptService
$this->em = $entityManager; $this->em = $entityManager;
} }
/**
* دریافت تمام ابزارهای بخش کالاها برای function calling
* @return array
*/
public function getTools(): array public function getTools(): array
{ {
$tools = []; $tools = [];
$commodityPrompt = $this->getAddOrUpdateCommodityPrompt();
// ابزار getItemInfo $commodityData = json_decode($commodityPrompt, true);
$itemInfoPrompt = $this->getItemInfoPrompt(); if ($commodityData) {
$itemInfoData = json_decode($itemInfoPrompt, true);
if ($itemInfoData) {
// اصلاح ساختار properties
$properties = [
'code' => [
'type' => 'string',
'description' => 'Item code (e.g., 1001, 1002)'
]
];
$tools[] = [ $tools[] = [
'type' => 'function', 'type' => 'function',
'function' => [ 'function' => [
'name' => $itemInfoData['tool'], 'name' => $commodityData['tool'],
'description' => $itemInfoData['description'], 'description' => $commodityData['description'],
'parameters' => [ 'parameters' => $commodityData['parameters']
'type' => 'object',
'properties' => $properties,
'required' => ['code']
]
] ]
]; ];
} }
return $tools; return $tools;
} }
/**
* تولید تمام پرامپ‌های بخش کالاها
* @return string
*/
public function getAllInventoryPrompts(): string public function getAllInventoryPrompts(): string
{ {
$prompts = []; return $this->getAddOrUpdateCommodityPrompt();
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getItemInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreateItemPrompt();
// $prompts[] = $this->getUpdateItemPrompt();
// $prompts[] = $this->getSearchItemPrompt();
// $prompts[] = $this->getItemStockPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts);
} }
/** public function getAddOrUpdateCommodityPrompt(): string
* پرامپ برای دریافت اطلاعات کامل کالا
* @return string
*/
public function getItemInfoPrompt(): string
{ {
return '{ return '{
"tool": "getItemInfo", "tool": "addOrUpdateCommodity",
"description": "Get complete item information by code", "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/item/info/{code}", "endpoint": "/api/commodity/mod/{code}",
"method": "GET", "method": "POST",
"input": { "parameters": {
"code": "string - Item code (e.g., 1001, 1002)" "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": { "output": {
"id": "integer - Item ID", "Success": "boolean",
"code": "string - Item code", "result": "integer",
"name": "string - Item name", "code": "integer"
"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": { "examples": {
"input": {"code": "1001"}, "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": { "output": {"Success":true,"result":1,"code":2}
"id": 45,
"code": "1001",
"name": "لپ‌تاپ اپل",
"description": "لپ‌تاپ اپل مک‌بوک پرو 13 اینچ",
"category": "الکترونیک",
"unit": "عدد",
"price": 45000000,
"stock": 15,
"minStock": 5,
"maxStock": 50,
"supplier": "شرکت اپل",
"barcode": "1234567890123",
"isActive": true
} }
} }';
}';
} }
} }

View file

@ -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; return $tools;
} }
@ -57,17 +84,9 @@ class PersonPromptService
public function getAllPersonPrompts(): string public function getAllPersonPrompts(): string
{ {
$prompts = []; $prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getPersonInfoPrompt(); $prompts[] = $this->getPersonInfoPrompt();
$prompts[] = $this->getPersonsListPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد $prompts[] = $this->getAddOrUpdatePersonPrompt();
// $prompts[] = $this->getCreatePersonPrompt();
// $prompts[] = $this->getUpdatePersonPrompt();
// $prompts[] = $this->getSearchPersonPrompt();
// $prompts[] = $this->getDeletePersonPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts); 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}
}
}';
}
} }

View file

@ -5,6 +5,7 @@ namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use App\Service\AGI\Promps\InventoryPromptService; use App\Service\AGI\Promps\InventoryPromptService;
use App\Service\AGI\Promps\BankPromptService; use App\Service\AGI\Promps\BankPromptService;
use App\Service\AGI\Promps\AccountingDocPromptService;
class PromptService class PromptService
{ {
@ -13,19 +14,22 @@ class PromptService
private $basePromptService; private $basePromptService;
private $inventoryPromptService; private $inventoryPromptService;
private $bankPromptService; private $bankPromptService;
private $accountingDocPromptService;
public function __construct( public function __construct(
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
PersonPromptService $personPromptService, PersonPromptService $personPromptService,
BasePromptService $basePromptService, BasePromptService $basePromptService,
InventoryPromptService $inventoryPromptService, InventoryPromptService $inventoryPromptService,
BankPromptService $bankPromptService BankPromptService $bankPromptService,
AccountingDocPromptService $accountingDocPromptService
) { ) {
$this->em = $entityManager; $this->em = $entityManager;
$this->personPromptService = $personPromptService; $this->personPromptService = $personPromptService;
$this->basePromptService = $basePromptService; $this->basePromptService = $basePromptService;
$this->inventoryPromptService = $inventoryPromptService; $this->inventoryPromptService = $inventoryPromptService;
$this->bankPromptService = $bankPromptService; $this->bankPromptService = $bankPromptService;
$this->accountingDocPromptService = $accountingDocPromptService;
} }
/** /**
@ -48,9 +52,9 @@ class PromptService
$bankTools = $this->bankPromptService->getTools(); $bankTools = $this->bankPromptService->getTools();
$tools = array_merge($tools, $bankTools); $tools = array_merge($tools, $bankTools);
// در آینده ابزارهای بخش‌های دیگر اضافه خواهند شد // ابزارهای بخش اسناد حسابداری
// $accountingTools = $this->accountingPromptService->getTools(); $accountingTools = $this->accountingDocPromptService->getTools();
// $tools = array_merge($tools, $accountingTools); $tools = array_merge($tools, $accountingTools);
return $tools; return $tools;
} }
@ -108,6 +112,9 @@ class PromptService
// پرامپ‌های بخش بانک‌ها // پرامپ‌های بخش بانک‌ها
$prompts['bank'] = $this->bankPromptService->getAllBankPrompts(); $prompts['bank'] = $this->bankPromptService->getAllBankPrompts();
// پرامپ‌های بخش اسناد حسابداری
$prompts['accounting'] = $this->accountingDocPromptService->getAllAccountingDocPrompts();
// در آینده بخش‌های دیگر اضافه خواهند شد // در آینده بخش‌های دیگر اضافه خواهند شد
// $prompts['accounting'] = $this->accountingPromptService->getAllAccountingPrompts(); // $prompts['accounting'] = $this->accountingPromptService->getAllAccountingPrompts();
// $prompts['reports'] = $this->reportsPromptService->getAllReportsPrompts(); // $prompts['reports'] = $this->reportsPromptService->getAllReportsPrompts();

View file

@ -73,6 +73,7 @@ export default defineComponent({
inputTokenPrice: 0, inputTokenPrice: 0,
outputTokenPrice: 0, outputTokenPrice: 0,
aiPrompt: '', aiPrompt: '',
aiDebugMode: false,
aiAgentSources: [ aiAgentSources: [
{ title: 'GapGPT', value: 'gapgpt', subtitle: 'gapgpt.app' }, { title: 'GapGPT', value: 'gapgpt', subtitle: 'gapgpt.app' },
{ title: 'AvalAI', value: 'avalai', subtitle: 'avalai.ir' }, { title: 'AvalAI', value: 'avalai', subtitle: 'avalai.ir' },
@ -164,6 +165,7 @@ export default defineComponent({
this.inputTokenPrice = parseFloat(data.inputTokenPrice) || 0; this.inputTokenPrice = parseFloat(data.inputTokenPrice) || 0;
this.outputTokenPrice = parseFloat(data.outputTokenPrice) || 0; this.outputTokenPrice = parseFloat(data.outputTokenPrice) || 0;
this.aiPrompt = data.aiPrompt || ''; this.aiPrompt = data.aiPrompt || '';
this.aiDebugMode = data.aiDebugMode === '1' || data.aiDebugMode === true;
this.loading = false; this.loading = false;
}) })
}, },
@ -224,7 +226,8 @@ export default defineComponent({
localModelAddress: this.localModelAddress, localModelAddress: this.localModelAddress,
inputTokenPrice: this.inputTokenPrice, inputTokenPrice: this.inputTokenPrice,
outputTokenPrice: this.outputTokenPrice, outputTokenPrice: this.outputTokenPrice,
aiPrompt: this.aiPrompt aiPrompt: this.aiPrompt,
aiDebugMode: this.aiDebugMode
}; };
axios.post('/api/admin/settings/system/info/save', submitData).then((resp) => { axios.post('/api/admin/settings/system/info/save', submitData).then((resp) => {
@ -876,6 +879,19 @@ export default defineComponent({
<v-icon size="16" class="mr-1">mdi-information</v-icon> <v-icon size="16" class="mr-1">mdi-information</v-icon>
این متن قبل از هر سوال به هوش مصنوعی ارسال میشود تا رفتار و پاسخدهی آن را کنترل کند این متن قبل از هر سوال به هوش مصنوعی ارسال میشود تا رفتار و پاسخدهی آن را کنترل کند
</div> </div>
<v-switch
v-model="aiDebugMode"
label="نمایش اطلاعات دیباگ در خروجی هوش مصنوعی"
color="info"
hide-details="auto"
inset
density="comfortable"
class="mt-6"
></v-switch>
<div class="text-caption text-medium-emphasis mt-1 d-flex align-center">
<v-icon size="16" class="mr-1">mdi-information</v-icon>
اگر این گزینه فعال باشد، اطلاعات دیباگ (debug_info) در خروجی پاسخهای هوش مصنوعی نمایش داده میشود.
</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>

View file

@ -1,6 +1,105 @@
<template> <template>
<div class="chat-container"> <div class="chat-container">
<!-- لودینگ بالای صفحه برای عملیات گفتگو -->
<v-progress-linear
v-if="loadingConversation"
color="primary"
indeterminate
absolute
style="top:0; left:0; right:0; z-index:2000;"
/>
<!-- دیالوگ مدیریت گفتوگوها - ساختار استاندارد Vuetify -->
<v-dialog v-model="showConversations" max-width="420" scrollable transition="dialog-bottom-transition">
<v-card>
<v-toolbar color="primary" dark flat rounded>
<v-avatar color="white" size="36" class="mr-3"><v-icon color="primary">mdi-forum</v-icon></v-avatar>
<v-toolbar-title class="font-weight-bold">مدیریت گفتوگوها</v-toolbar-title>
<v-spacer></v-spacer>
<!-- دکمه حذف همه گفتوگوها با تولتیپ -->
<v-tooltip text="حذف همه گفت‌وگوها" location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
color="error"
:loading="loadingDeleteAll"
icon
class="ml-1"
@click="showDeleteAllDialog = true"
>
<v-icon>mdi-delete-sweep</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- دکمه بستن دیالوگ -->
<v-btn icon variant="text" @click="showConversations = false"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar>
<v-divider></v-divider>
<v-list class="py-0" style="min-height: 320px; max-height: 60vh; overflow-y: auto;">
<template v-if="conversations.length">
<template v-for="(conv, idx) in conversations" :key="conv.id">
<v-list-item :active="conv.id === conversationId" @click="switchConversation(conv.id)" class="conv-list-item-v">
<template #prepend>
<v-avatar :color="getAvatarColor(conv.title)" size="36">
<span class="white--text text-h6">{{ conv.title.charAt(0) }}</span>
</v-avatar>
</template>
<v-list-item-title class="font-weight-bold">{{ conv.title }}</v-list-item-title>
<v-list-item-subtitle class="d-flex align-center">
<v-icon size="16" color="grey" class="ml-1">mdi-clock-outline</v-icon>
<span class="mr-1">{{ conv.updatedAt }}</span>
<v-divider vertical class="mx-2" style="height: 18px;"></v-divider>
<v-icon size="16" color="primary" class="ml-1">mdi-message-reply-text</v-icon>
<span>{{ conv.messageCount }}</span>
</v-list-item-subtitle>
<template #append>
<v-btn icon color="error" variant="text" @click.stop="confirmDelete(conv.id)"><v-icon>mdi-delete</v-icon></v-btn>
</template>
</v-list-item>
<v-divider v-if="idx !== conversations.length - 1" class="my-1"></v-divider>
</template>
</template>
<template v-else>
<div class="d-flex flex-column align-center justify-center py-10">
<v-icon size="64" color="grey-lighten-1">mdi-emoticon-happy-outline</v-icon>
<div class="mt-3 text-h6">هنوز گفتوگویی ایجاد نکردهاید!</div>
<div class="text-caption mt-1">برای شروع، روی دکمه زیر کلیک کنید.</div>
</div>
</template>
</v-list>
<v-divider></v-divider>
<v-card-actions class="pa-4 d-flex flex-row-reverse align-center justify-space-between">
<v-btn color="primary" block large class="font-weight-bold" @click="createConversation">
<v-icon start>mdi-plus</v-icon>
گفتوگوی جدید
</v-btn>
<!-- دکمه حذف همه را از پایین (v-card-actions) حذف کن -->
</v-card-actions>
<!-- دیالوگ تایید حذف -->
<v-dialog v-model="showDeleteDialog" max-width="320">
<v-card>
<v-card-title class="text-h6">حذف گفتگو</v-card-title>
<v-card-text>آیا از حذف این گفتگو مطمئن هستید؟ این عمل قابل بازگشت نیست.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDeleteDialog = false">انصراف</v-btn>
<v-btn color="error" variant="flat" :loading="loadingDelete" :disabled="loadingDelete" @click="doDeleteConversation">حذف</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- دیالوگ تایید حذف همه -->
<v-dialog v-model="showDeleteAllDialog" max-width="340">
<v-card>
<v-card-title class="text-h6">حذف همه گفتوگوها</v-card-title>
<v-card-text>آیا از حذف همه گفتوگوها مطمئن هستید؟ این عمل قابل بازگشت نیست.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDeleteAllDialog = false">انصراف</v-btn>
<v-btn color="error" variant="flat" :loading="loadingDeleteAll" :disabled="loadingDeleteAll" @click="deleteAllConversations">حذف همه</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</v-dialog>
<!-- ناحیه پیامها --> <!-- ناحیه پیامها -->
<div class="messages-container" ref="messagesContainer"> <div class="messages-container" ref="messagesContainer">
@ -83,6 +182,21 @@
> >
<v-icon>mdi-send</v-icon> <v-icon>mdi-send</v-icon>
</v-btn> </v-btn>
<!-- دکمه مدیریت گفتوگوها فقط آیکون با تولتیپ -->
<v-tooltip text="مدیریت گفت‌وگوها" location="top">
<template #activator="{ props }">
<v-btn
v-bind="props"
color="info"
size="small"
class="send-button"
@click="showConversations = true"
icon
>
<v-icon>mdi-forum</v-icon>
</v-btn>
</template>
</v-tooltip>
</div> </div>
<!-- دکمههای سریع --> <!-- دکمههای سریع -->
@ -111,6 +225,9 @@
</v-alert> </v-alert>
</div> </div>
</div> </div>
<v-snackbar v-model="showSnackbar" :color="snackbarColor" location="bottom" timeout="3500">
{{ snackbarText }}
</v-snackbar>
</div> </div>
</template> </template>
@ -118,6 +235,7 @@
import { useNavigationStore } from '@/stores/navigationStore'; import { useNavigationStore } from '@/stores/navigationStore';
import axios from 'axios'; import axios from 'axios';
import AIChart from '@/components/widgets/AIChart.vue'; import AIChart from '@/components/widgets/AIChart.vue';
import { h } from 'vue';
export default { export default {
name: "WizardHome", name: "WizardHome",
@ -137,12 +255,23 @@ export default {
aiEnabled: false, aiEnabled: false,
aiStatus: 'checking', aiStatus: 'checking',
conversationId: null, conversationId: null,
showConversations: false,
conversations: [],
quickSuggestions: [ quickSuggestions: [
'چطور می‌تونم کمکتون کنم؟', 'چگونه می‌توانم یک گزارش مالی تهیه کنم؟',
'سوالی دارید؟', 'برای ثبت یک فاکتور جدید چه مراحلی را باید طی کنم؟',
'نیاز به راهنمایی دارید؟', 'چطور می‌توانم کاربران جدید به سیستم اضافه کنم؟',
'مشکلی پیش اومده؟' 'چگونه می‌توانم تنظیمات کسب‌وکارم را تغییر دهم؟'
] ],
showDeleteDialog: false,
deleteTargetId: null,
loadingConversation: false,
loadingDelete: false,
showSnackbar: false,
snackbarText: '',
snackbarColor: 'success',
showDeleteAllDialog: false,
loadingDeleteAll: false,
} }
}, },
methods: { methods: {
@ -350,8 +479,206 @@ export default {
} }
}, 100); }, 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() { async mounted() {
// بستن منو در دسکتاپ // بستن منو در دسکتاپ
@ -362,6 +689,7 @@ export default {
// بررسی وضعیت هوش مصنوعی // بررسی وضعیت هوش مصنوعی
await this.checkAIStatus(); await this.checkAIStatus();
await this.fetchConversations();
this.scrollToBottom(); this.scrollToBottom();
}, },
@ -602,4 +930,185 @@ export default {
max-width: 90%; 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;
}
</style> </style>