model.php 85 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172
  1. <?php
  2. /**
  3. * The model file of zai module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2025 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author tenghuaian <tenghuaian@chandao.com>
  8. * @package zai
  9. * @link https://www.zentao.net
  10. */
  11. class zaiModel extends model
  12. {
  13. /**
  14. * 获取当前用户ZAI Authorization Token。
  15. * Get ZAI Authorization Token of current user.
  16. *
  17. * @access public
  18. * @param object|null $zaiConfig
  19. * @param bool $admin
  20. * @return array
  21. */
  22. public function getToken($zaiConfig = null, $admin = false)
  23. {
  24. $zaiConfig = $zaiConfig ? $zaiConfig : $this->getSetting($admin);
  25. if(!isset($zaiConfig->token) || !isset($zaiConfig->appID) || ($admin && !isset($zaiConfig->adminToken)))
  26. {
  27. return array(
  28. 'result' => 'fail',
  29. 'message' => $this->lang->zai->configurationUnavailable
  30. );
  31. }
  32. if(!$this->loadModel('user')->isLogon())
  33. {
  34. return array(
  35. 'result' => 'fail',
  36. 'message' => $this->lang->zai->illegalZentaoUser
  37. );
  38. }
  39. $token = $admin ? $zaiConfig->adminToken : $zaiConfig->token;
  40. $userID = $this->app->user->account;
  41. $zaiTokenTTL = isset($zaiConfig->zaiTokenTTL) ? $zaiConfig->zaiTokenTTL : 1200;
  42. $expiredTime = time() + $zaiTokenTTL;
  43. $origin = $token . $zaiConfig->appID . $userID . $expiredTime;
  44. $hash = md5($origin);
  45. return array(
  46. 'result' => 'success',
  47. 'data' => array(
  48. 'hash' => $hash,
  49. 'expiredTime' => $expiredTime,
  50. 'appID' => $zaiConfig->appID,
  51. 'userID' => $userID
  52. )
  53. );
  54. }
  55. /**
  56. * 格式化旧的ZAI设置。
  57. * Format old ZAI settings.
  58. *
  59. * @access public
  60. * @param object|null $setting
  61. * @return object|null
  62. */
  63. public function formatOldSetting($setting)
  64. {
  65. if(empty($setting)) return null;
  66. if(empty($setting->host) && !empty($setting->apiBaseUrl))
  67. {
  68. $apiBaseUrl = str_replace('///', '', $setting->apiBaseUrl);
  69. $apiBaseUrl = str_replace('http://', '', $apiBaseUrl);
  70. $apiBaseUrl = str_replace('https://', '', $apiBaseUrl);
  71. $apiBaseUrl = str_replace('/v1', '', $apiBaseUrl);
  72. $urlParts = explode(':', $apiBaseUrl);
  73. $setting->host = $urlParts[0];
  74. $setting->port = isset($urlParts[1]) ? $urlParts[1] : 0;
  75. }
  76. if(empty($setting->token) && !empty($setting->appToken))
  77. {
  78. $setting->token = $setting->appToken;
  79. unset($setting->appToken);
  80. }
  81. return $setting;
  82. }
  83. /**
  84. * 获取ZAI设置。
  85. * Get ZAI settings.
  86. *
  87. * @access public
  88. * @param bool $includeAdmin
  89. * @return object|null
  90. */
  91. public function getSetting($includeAdmin = false)
  92. {
  93. $settingJson = $this->loadModel('setting')->getItem("owner=system&module=zai&section=global&key=setting");
  94. $setting = json_decode($settingJson);
  95. if(!is_object($setting) && isset($this->config->zai))
  96. {
  97. $setting = $this->formatOldSetting($this->config->zai);
  98. }
  99. if(empty($setting) || empty($setting->host) || empty($setting->appID) || empty($setting->token))
  100. {
  101. return null;
  102. }
  103. $vectorizedInfo = $this->getVectorizedInfo();
  104. if(!empty($vectorizedInfo->key))
  105. {
  106. $setting->globalMemory = 'zentao:global';
  107. }
  108. if(!$includeAdmin) unset($setting->adminToken);
  109. return $setting;
  110. }
  111. /**
  112. * 设置ZAI设置。
  113. * Set ZAI settings.
  114. *
  115. * @access public
  116. * @param object|null $setting
  117. */
  118. public function setSetting($setting)
  119. {
  120. if(!is_object($setting))
  121. {
  122. $this->loadModel('setting')->setItem('system.zai.global.setting', '');
  123. return;
  124. }
  125. $this->loadModel('setting')->setItem('system.zai.global.setting', empty($setting) ? '' : json_encode($setting));
  126. }
  127. /**
  128. * 获取禅道数据向量化信息。
  129. * Get information of vectorized data of ZenTao.
  130. *
  131. * @access public
  132. * @return object
  133. */
  134. public function getVectorizedInfo()
  135. {
  136. $infoJson = $this->loadModel('setting')->getItem("owner=system&module=zai&section=kb&key=systemVectorization");
  137. $info = json_decode($infoJson);
  138. if(!$info)
  139. {
  140. $info = new stdClass();
  141. $info->key = '';
  142. $info->status = 'disabled';
  143. $info->syncedTime = 0;
  144. $info->syncedCount = 0;
  145. $info->syncFailedCount = 0;
  146. $info->syncTime = 0;
  147. $info->syncingType = zaiModel::getNextSyncType();
  148. $info->syncingID = 0;
  149. $info->syncDetails = new stdClass();
  150. $info->createdAt = time();
  151. $info->createdBy = $this->app->user->account;
  152. }
  153. return $info;
  154. }
  155. /**
  156. * 设置禅道数据向量化信息。
  157. * Set information of vectorized data of ZenTao.
  158. *
  159. * @access public
  160. * @param object $info - The information of vectorized data. 禅道数据向量化信息对象。
  161. */
  162. public function setVectorizedInfo($info)
  163. {
  164. if(!is_string($info)) $info = json_encode($info);
  165. $this->loadModel('setting')->setItem('system.zai.kb.systemVectorization', $info);
  166. }
  167. /**
  168. * 调用 ZAI API。
  169. * Call ZAI API.
  170. *
  171. * @access public
  172. * @param string $path
  173. * @param string $method
  174. * @param array|null $params
  175. * @param array|null $postData
  176. * @param bool $admin
  177. * @return array 通过 result 属性返回调用结果,通过 data 属性返回调用数据,通过 message 属性返回调用错误信息,通过 code 属性返回调用错误代码。 Return array with result, data, message and code.
  178. */
  179. public function callAPI($path, $method = 'POST', $params = null, $postData = null, $admin = false)
  180. {
  181. $setting = $this->getSetting($admin);
  182. if(empty($setting)) return array('result' => 'fail', 'message' => $this->lang->zai->configurationUnavailable);
  183. $tokenInfo = $this->getToken($setting, $admin);
  184. if($tokenInfo['result'] != 'success') return $tokenInfo;
  185. $tokenData = array('hash' => $tokenInfo['data']['hash'], 'expired_time' => $tokenInfo['data']['expiredTime'], 'app_id' => $tokenInfo['data']['appID'], 'user_id' => $tokenInfo['data']['userID']);
  186. $token = ($admin ? 'ak-' : 'ek-') . base64_encode(json_encode($tokenData));
  187. /* Check if the request is HTTPS. */
  188. if($path[0] != '/') $path = '/' . $path;
  189. $isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
  190. if(!$isHttps) $isHttps = !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https';
  191. if(!$isHttps) $isHttps = !empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443;
  192. $protocol = $isHttps ? 'https://' : 'http://';
  193. $host = $setting->host;
  194. $port = $setting->port;
  195. $url = $protocol . $host . ($port ? ":$port" : '') . $path;
  196. if($params) $url = $url . '?' . http_build_query($params);
  197. $curl = curl_init($url);
  198. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  199. curl_setopt($curl, CURLOPT_HEADER, true);
  200. if($method === 'GET')
  201. {
  202. curl_setopt($curl, CURLOPT_HTTPGET, true);
  203. }
  204. elseif($method === 'POST')
  205. {
  206. curl_setopt($curl, CURLOPT_POST, true);
  207. }
  208. else
  209. {
  210. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
  211. }
  212. $hasFile = false;
  213. if($postData !== null) // Post data must set even if it is empty
  214. {
  215. foreach($postData as $value)
  216. {
  217. if($value instanceof CURLFile)
  218. {
  219. $hasFile = true;
  220. break;
  221. }
  222. }
  223. curl_setopt($curl, CURLOPT_POSTFIELDS, $hasFile ? $postData : json_encode($postData, JSON_UNESCAPED_UNICODE));
  224. }
  225. $headers = ['Authorization: Bearer ' . $token];
  226. if(!$hasFile) $headers[] = 'Content-Type: application/json';
  227. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  228. $response = curl_exec($curl);
  229. $error = '';
  230. $data = null;
  231. $info = curl_getinfo($curl);
  232. $code = isset($info['http_code']) ? $info['http_code'] : curl_getinfo($curl, CURLINFO_HTTP_CODE);
  233. if(curl_errno($curl))
  234. {
  235. $error = curl_error($curl);
  236. }
  237. else
  238. {
  239. $headerSize = isset($info['header_size']) ? $info['header_size'] : curl_getinfo($curl, CURLINFO_HEADER_SIZE);
  240. $data = substr($response, $headerSize);
  241. }
  242. curl_close($curl);
  243. if($code == 404) return array('result' => 'fail', 'data' => null, 'message' => $this->lang->notFound, 'code' => $code);
  244. if($code == 401) return array('result' => 'fail', 'data' => null, 'message' => $this->lang->zai->authenticationFailed, 'code' => $code);
  245. if($error || $code != 200)
  246. {
  247. return array('result' => 'fail', 'data' => $data, 'code' => $code, 'postData' => $postData, 'message' => sprintf($this->lang->zai->callZaiAPIFailed, $url, ($this->app->config->debug ? $error : '') . "(code: $code, response: $response)"));
  248. }
  249. if(empty($data)) $data = $response;
  250. if(is_string($data)) $data = json_decode($data, true);
  251. return array('result' => 'success', 'data' => $data);
  252. }
  253. /**
  254. * 调用ZAI管理API。
  255. * Call ZAI admin API.
  256. *
  257. * @access public
  258. * @param string $path
  259. * @param string $method
  260. * @param array|null $params
  261. * @param array|null $postData
  262. * @return array 通过 result 属性返回调用结果,通过 data 属性返回调用数据,通过 message 属性返回调用错误信息,通过 code 属性返回调用错误代码。 Return array with result, data, message and code.
  263. */
  264. public function callAdminAPI($path, $method = 'POST', $params = null, $postData = null)
  265. {
  266. return $this->callAPI($path, $method, $params, $postData, true);
  267. }
  268. /**
  269. * 启用数据向量化。
  270. * Enable data vectorization.
  271. *
  272. * @access public
  273. * @return array
  274. */
  275. public function enableVectorization($force = false)
  276. {
  277. $info = $this->getVectorizedInfo();
  278. if($info->status != 'disabled' && !$force) return array('result' => 'fail', 'message' => $this->lang->zai->vectorizedAlreadyEnabled, 'info' => $info);
  279. $suffix = '_' . time();
  280. $postData = array('name' => 'zentao' . $suffix, 'description' => $this->app->lang->zai->zentaoVectorization);
  281. $result = $this->callAdminAPI('/v8/memories', 'POST', null, $postData);
  282. if($result['result'] != 'success') return $result;
  283. if(empty($result['data']['id'])) return array('result' => 'fail', 'message' => $this->lang->zai->vectorizedFailed);
  284. $info->status = 'wait';
  285. $info->key = $result['data']['id'];
  286. $info->syncedTime = 0;
  287. $info->syncedCount = 0;
  288. $info->syncFailedCount = 0;
  289. $info->syncTime = 0;
  290. $info->syncingType = zaiModel::getNextSyncType();
  291. $info->syncingID = 0;
  292. $info->syncDetails = new stdClass();
  293. $this->setVectorizedInfo($info);
  294. return array('result' => 'success', 'info' => $info, 'data' => $result['data'], 'message' => $this->lang->zai->vectorizedEnabled);
  295. }
  296. /**
  297. * 获取下一个指定类型的对象。
  298. * 从数据库查询对应 $syncingType 的下一个待同步的对象,即 ID 大于 $syncingID 的且没有被删除的下一个对象。如果未查询到,则返回 null。
  299. *
  300. * @access public
  301. * @param string $type
  302. * @param int $id
  303. * @return object|null
  304. */
  305. public function getNextTarget($type, $id)
  306. {
  307. if(!isset(static::$syncTables[$type])) return null;
  308. $table = static::$syncTables[$type];
  309. $target = $this->dao->select('*')->from($table)->where('id')->ge($id)->andWhere('deleted')->eq(0)->orderby('id_asc')->fetch();
  310. if(!$target) return null;
  311. return $target;
  312. }
  313. /**
  314. * 同步下一个目标。
  315. * Sync next target.
  316. *
  317. * @access public
  318. * @param string $memoryID 知识库 ID。The ID of knowledge base.
  319. * @param string $type
  320. * @param int $id
  321. * @return array|null
  322. */
  323. public function syncNextTarget($memoryID, $type, $id)
  324. {
  325. $target = $this->getNextTarget($type, $id);
  326. if(!$target) return null;
  327. $markdownData = static::convertTargetToMarkdown($type, $target);
  328. $syncData = array();
  329. $syncData['content'] = $markdownData['content'];
  330. $syncData['content_type'] = 'markdown';
  331. $syncData['key'] = "$type-$target->id";
  332. $syncData['attrs'] = array('objectType' => $type, 'objectID' => $target->id);
  333. if(isset($markdownData['attrs'])) $syncData['attrs'] = array_merge($syncData['attrs'], $markdownData['attrs']);
  334. $result = $this->callAdminAPI("/v8/memories/$memoryID/contents", 'POST', null, $syncData);
  335. if($result['result'] != 'success')
  336. {
  337. $code = isset($result['code']) ? $result['code'] : 0;
  338. /* 当指定 memoryID 的知识库不存在时创建一个新的。 */
  339. /* If kb with the given memoryID not exists, then create a new one. */
  340. if($code == 412)
  341. {
  342. $result['message'] = $this->lang->zai->cannotFindMemoryInZai;
  343. $result['code'] = 'cannotFindMemoryInZai';
  344. }
  345. if($code !== 100 && ($code >= 400 || $code <= 500)) $result['fatal'] = true;
  346. return $result;
  347. }
  348. return array('result' => 'success', 'target' => $target, 'id' => $target->id, 'data' => $result['data'], 'syncedData' => $syncData);
  349. }
  350. /**
  351. * 获取知识库在 ZAI 的 key。
  352. * Get key of knowledge base in ZAI.
  353. *
  354. * @access public
  355. * @param string|int $libID
  356. * @return string
  357. */
  358. public function getCollectionKey($libID)
  359. {
  360. if($libID === 'global')
  361. {
  362. $vectorizedInfo = $this->getVectorizedInfo();
  363. return empty($vectorizedInfo->key) ? '' : $vectorizedInfo->key;
  364. }
  365. return '';
  366. }
  367. /**
  368. * 创建知识库。
  369. * Create knowledge library.
  370. *
  371. * @access public
  372. * @param string $name
  373. * @param string $description
  374. * @param array|null $options
  375. * @return array 通过 result 属性返回调用结果,通过 data 属性返回调用数据,通过 message 属性返回调用错误信息,通过 code 属性返回调用错误代码。 Return array with result, data, message and code.
  376. */
  377. public function createKnowledgeLib($name, $description = '', $options = null)
  378. {
  379. $postData = ['name' => $name, 'description' => $description];
  380. if($options) $postData = array_merge($postData, $options);
  381. $result = $this->callAdminAPI('/v8/memories', 'POST', null, $postData);
  382. if($result['result'] !== 'success' || empty($result['data']['id'])) return null;
  383. return $result['data'];
  384. }
  385. /**
  386. * 删除知识库。
  387. * Delete knowledge library.
  388. *
  389. * @access public
  390. * @param string $memoryID
  391. * @return bool
  392. */
  393. public function deleteKnowledgeLib($memoryID)
  394. {
  395. $result = $this->callAdminAPI("/v8/memories/$memoryID", 'DELETE');
  396. return $result['result'] === 'success';
  397. }
  398. /**
  399. * 更新知识内容。
  400. * Update knowledge content.
  401. *
  402. * @access public
  403. * @param string $memoryID
  404. * @param string $key
  405. * @param string $content
  406. * @param string $contentType
  407. * @param array|null $attrs
  408. * @return array|null
  409. */
  410. public function updateKnowledgeItem($memoryID, $key, $content, $contentType = 'markdown', $attrs = null)
  411. {
  412. $postData = ['content' => $content, 'content_type' => $contentType, 'key' => $key, 'attrs' => $attrs];
  413. $result = $this->callAdminAPI("/v8/memories/$memoryID/contents", 'POST', null, $postData);
  414. if($result['result'] === 'success')
  415. {
  416. if(!empty($result['data']['content_id'])) return $result['data']['content_id'];
  417. $contentsResult = $this->callAdminAPI("/v8/memories/$memoryID/contents", 'GET');
  418. if($contentsResult['result'] !== 'success') return null;
  419. $contentList = empty($contentsResult['data']) ? array() : $contentsResult['data'];
  420. foreach($contentList as $content)
  421. {
  422. if($content['key'] === $key) return $content['id'];
  423. }
  424. }
  425. return null;
  426. }
  427. /**
  428. * 删除知识内容。
  429. * Delete knowledge content.
  430. *
  431. * @access public
  432. * @param string $memoryID
  433. * @param string $contentID
  434. * @return bool
  435. */
  436. public function deleteKnowledgeItem($memoryID, $contentID)
  437. {
  438. $result = $this->callAdminAPI("/v8/memories/$memoryID/contents/$contentID", 'DELETE');
  439. return $result['result'] === 'success';
  440. }
  441. /**
  442. * 获取知识内容块列表。
  443. * Get list of knowledge content chunks.
  444. *
  445. * @access public
  446. * @param string $memoryID
  447. * @param string $contentID
  448. * @return array|null
  449. */
  450. public function getKnowledgeChunks($memoryID, $contentID)
  451. {
  452. $result = $this->callAdminAPI("/v8/memories/$memoryID/contents/$contentID/chunks", 'GET');
  453. if($result['result'] !== 'success') return null;
  454. return empty($result['data']) ? array() : $result['data'];
  455. }
  456. /**
  457. * 搜索知识库。
  458. * Search knowledge base.
  459. *
  460. * @access public
  461. * @param string $query
  462. * @param string $collection
  463. * @param array $filter
  464. * @param int $limit
  465. * @param float $minSimilarity
  466. * @param bool $filterByPriv
  467. * @return array
  468. */
  469. public function searchKnowledges($query, $collection, $filter, $limit = 20, $minSimilarity = 0.5, $filterByPriv = true)
  470. {
  471. $postData = array();
  472. $postData['query'] = $query;
  473. $postData['limit'] = $limit;
  474. $postData['min_similarity'] = $minSimilarity;
  475. $postData['content_filter'] = $filter;
  476. $result = $this->callAdminAPI('/v8/memories/' . $collection . '/embeddings-search-contents', 'POST', null, $postData);
  477. if($result['result'] != 'success') return array();
  478. $knowledges = empty($result['data']) ? array() : $result['data'];
  479. if($filterByPriv) $knowledges = $this->filterKnowledgesByPriv($knowledges, 'content');
  480. return $knowledges;
  481. }
  482. /**
  483. * 搜索知识块。
  484. * Search knowledge chunks.
  485. *
  486. * @access public
  487. * @param string $query
  488. * @param string $collection
  489. * @param array $filter
  490. * @param int $limit
  491. * @param float $minSimilarity
  492. * @param bool $filterByPriv
  493. * @return array
  494. */
  495. public function searchKnowledgeChunks($query, $collection, $filter = array(), $limit = 20, $minSimilarity = 0.5, $filterByPriv = true)
  496. {
  497. $postData = array();
  498. $postData['query'] = $query;
  499. $postData['limit'] = $limit;
  500. $postData['min_similarity'] = $minSimilarity;
  501. $postData['content_filter'] = $filter;
  502. $result = $this->callAdminAPI('/v8/memories/' . $collection . '/embeddings-search-chunks', 'POST', null, $postData);
  503. if($result['result'] != 'success') return array();
  504. $chunks = empty($result['data']) ? array() : $result['data'];
  505. if($filterByPriv) $chunks = $this->filterKnowledgesByPriv($chunks, 'chunk');
  506. return $chunks;
  507. }
  508. /**
  509. * 在多个知识库中搜索知识。
  510. * Search knowledges in multiple collections.
  511. *
  512. * @access public
  513. * @param string $query
  514. * @param string $type
  515. * @param array $filters
  516. * @param int $limit
  517. * @param float $minSimilarity
  518. * @return array
  519. */
  520. public function searchKnowledgesInCollections($query, $filters, $type = 'content', $limit = 20, $minSimilarity = 0.5)
  521. {
  522. $knowledges = array();
  523. foreach($filters as $collection => $setting)
  524. {
  525. $key = $this->getCollectionKey($collection);
  526. if(empty($key)) continue;
  527. /* 不进行权限过滤,按匹配度排序后会统一过滤,减少循环次数。 */
  528. $searchKnowledges = $type === 'chunk' ? $this->searchKnowledgeChunks($query, $key, $setting, $limit + 10, $minSimilarity, false) : $this->searchKnowledges($query, $key, $setting, $limit + 10, $minSimilarity, false); // 比预设的数目多搜索 10 个,避免因过滤导致无法匹配到。
  529. if($searchKnowledges) $knowledges = array_merge($knowledges, $searchKnowledges);
  530. }
  531. array_multisort(array_column($knowledges, 'similarity'), SORT_DESC, $knowledges);
  532. return $this->filterKnowledgesByPriv($knowledges, $type, $limit);
  533. }
  534. /**
  535. * 判断用户是否可以查看对象。
  536. * Check if user can view object.
  537. *
  538. * @access public
  539. * @param string $objectType
  540. * @param int $objectID
  541. * @return bool
  542. * @param mixed[]|null $attrs
  543. */
  544. public function canViewObject($objectType, $objectID, $attrs = null)
  545. {
  546. if($this->app->user->admin) return true;
  547. if(isset(static::$objectViews[$objectType][$objectID])) return static::$objectViews[$objectType][$objectID];
  548. if(!isset(static::$objectViews[$objectType])) static::$objectViews[$objectType] = array();
  549. if($attrs === null) $attrs = array();
  550. $canView = false;
  551. if($objectType === 'story' || $objectType === 'demand')
  552. {
  553. $table = $objectType === 'story' ? TABLE_STORY : TABLE_DEMAND;
  554. $product = isset($attrs['product']) ? $attrs['product'] : 0;
  555. if(!$product) $product = $this->dao->select('product')->from($table)->where('id')->eq($objectID)->fetch('product');
  556. $canView = strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false;
  557. }
  558. elseif($objectType === 'bug')
  559. {
  560. $product = isset($attrs['product']) ? $attrs['product'] : 0;
  561. if(!$product) $product = $this->dao->select('product')->from(TABLE_BUG)->where('id')->eq($objectID)->fetch('product');
  562. $canView = strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false;
  563. if(!$canView)
  564. {
  565. $project = isset($attrs['project']) ? $attrs['project'] : 0;
  566. if(!$project) $project = $this->dao->select('project')->from(TABLE_BUG)->where('id')->eq($objectID)->fetch('project');
  567. $canView = strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false;
  568. }
  569. }
  570. elseif($objectType === 'task')
  571. {
  572. $project = isset($attrs['project']) ? $attrs['project'] : 0;
  573. if(!$project) $project = $this->dao->select('project')->from(TABLE_TASK)->where('id')->eq($objectID)->fetch('project');
  574. $canView = strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false;
  575. }
  576. elseif($objectType === 'feedback')
  577. {
  578. $product = isset($attrs['product']) ? $attrs['product'] : 0;
  579. if(!$product) $product = $this->dao->select('product')->from(TABLE_FEEDBACK)->where('id')->eq($objectID)->fetch('product');
  580. $canView = strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false;
  581. }
  582. elseif($objectType === 'doc')
  583. {
  584. $api = isset($attrs['docType']) && $attrs['docType'] === 'api';
  585. $doc = $this->loadModel($api ? 'api' : 'doc')->getByID($objectID);
  586. $canView = $this->loadModel('doc')->checkPrivDoc($doc);
  587. }
  588. elseif($objectType === 'case')
  589. {
  590. $case = $this->loadModel('testcase')->getById($objectID);
  591. if(!$case)
  592. {
  593. static::$objectViews[$objectType][$objectID] = false;
  594. return false;
  595. }
  596. $project = isset($case->project) ? $case->project : 0;
  597. $product = isset($case->product) ? $case->product : 0;
  598. if($project && strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false) $canView = true;
  599. if(!$canView && $product && strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false) $canView = true;
  600. if($canView)
  601. {
  602. static::$objectViews[$objectType][$objectID] = true;
  603. return true;
  604. }
  605. $libID = $case->lib;
  606. if($libID)
  607. {
  608. $lib = $this->loadModel('caselib')->getByID($libID);
  609. if($lib)
  610. {
  611. $project = isset($lib->project) ? $lib->project : 0;
  612. $product = isset($lib->product) ? $lib->product : 0;
  613. if($project && strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false) $canView = true;
  614. if(!$canView && $product && strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false) $canView = true;
  615. }
  616. }
  617. }
  618. static::$objectViews[$objectType][$objectID] = $canView;
  619. return $canView;
  620. }
  621. /**
  622. * 过滤出有权限的知识。
  623. * Filter knowledges which has privilege.
  624. *
  625. * @access public
  626. * @param array $knowledges
  627. * @param string $type content|chunk
  628. * @param int $limit
  629. * @return array
  630. */
  631. public function filterKnowledgesByPriv($knowledges, $type = 'content', $limit = 0)
  632. {
  633. $filteredKnowledges = array();
  634. $isChunk = $type == 'chunk';
  635. $keyName = $isChunk ? 'content_key' : 'key';
  636. $attrsName = $isChunk ? 'content_attrs' : 'attrs';
  637. foreach($knowledges as $knowledge)
  638. {
  639. $key = isset($knowledge[$keyName]) ? $knowledge[$keyName] : '';
  640. if(empty($key)) continue;
  641. $keyParts = explode('-', $key);
  642. if(count($keyParts) < 2) continue;
  643. [$objectType, $objectID] = $keyParts;
  644. $attrs = isset($knowledge[$attrsName]) ? $knowledge[$attrsName] : null;
  645. if(!empty($attrs['objectType'])) $objectType = $attrs['objectType'];
  646. if(!empty($attrs['objectID'])) $objectID = $attrs['objectID'];
  647. if(!$this->canViewObject($objectType, $objectID, $attrs)) continue;
  648. $filteredKnowledges[] = $knowledge;
  649. if($limit > 0 && count($filteredKnowledges) >= $limit) break;
  650. }
  651. return $filteredKnowledges;
  652. }
  653. /**
  654. * 提取文件内容。
  655. * Extract file content.
  656. *
  657. * @access public
  658. * @param int|object $file
  659. * @return array
  660. */
  661. public function extractFileContent($file)
  662. {
  663. if(!is_object($file)) $file = $this->loadModel('file')->getByID($file);
  664. if(!is_object($file) || empty($file->realPath)) return null;
  665. $filePath = $file->realPath;
  666. $cFile = new CURLFile($filePath, null, $file->title);
  667. $result = $this->callAdminAPI('/v8/files/extract', 'POST', null, ['file' => $cFile]);
  668. if($result['result'] != 'success') return null;
  669. return $result['data'];
  670. }
  671. /**
  672. * 用户对象可查看缓存配置。
  673. * User object view cache configuration.
  674. *
  675. * @access public
  676. * @var array
  677. */
  678. static $objectViews = array();
  679. /**
  680. * 同步表。
  681. * Sync tables.
  682. *
  683. * @access public
  684. * @var array
  685. */
  686. static $syncTables = array
  687. (
  688. 'story' => TABLE_STORY,
  689. 'demand' => TABLE_DEMAND,
  690. 'bug' => TABLE_BUG,
  691. 'doc' => TABLE_DOC,
  692. 'design' => TABLE_DESIGN,
  693. 'feedback' => TABLE_FEEDBACK
  694. );
  695. /**
  696. * 获取可同步的类型。
  697. * Get syncable types.
  698. *
  699. * @access public
  700. * @return array
  701. */
  702. public static function getSyncTypes()
  703. {
  704. global $app, $config;
  705. $types = $app->lang->zai->syncingTypeList;
  706. if($config->edition == 'open') unset($types['feedback']);
  707. if($config->edition != 'ipd') unset($types['demand']);
  708. return $types;
  709. }
  710. /**
  711. * 获取下一个同步类型。
  712. * Get next sync type.
  713. *
  714. * @access public
  715. * @param string $currentType
  716. * @return string 下一个同步类型。The next sync type.
  717. */
  718. public static function getNextSyncType($currentType = '')
  719. {
  720. global $app;
  721. $types = array_keys(zaiModel::getSyncTypes());
  722. if(empty($currentType)) return $types[0];
  723. $currentIndex = array_search($currentType, $types);
  724. if($currentIndex === false) return $types[0];
  725. return isset($types[$currentIndex + 1]) ? $types[$currentIndex + 1] : null;
  726. }
  727. /**
  728. * 将目标对象转换为 Markdown 格式。
  729. * Convert target object to Markdown format.
  730. *
  731. * @access public
  732. * @param string $type
  733. * @param object $target
  734. * @param array $langData
  735. * @return array
  736. */
  737. public static function convertTargetToMarkdown($type, $target, $langData = [])
  738. {
  739. global $app;
  740. $funcName = 'convert' . ucfirst($type) . 'ToMarkdown';
  741. if(method_exists(static::class, $funcName))
  742. {
  743. $markdown = static::$funcName($target, $langData);
  744. }
  745. elseif($type === 'practice' || $type === 'component')
  746. {
  747. $markdown = static::convertDocToMarkdown($target);
  748. }
  749. else
  750. {
  751. $markdown = static::convertGenericToMarkdown($type, $target);
  752. }
  753. if(!isset($markdown['attrs'])) $markdown['attrs'] = array();
  754. if(!isset($markdown['attrs']['objectType'])) $markdown['attrs']['objectType'] = $type;
  755. if(!isset($markdown['attrs']['objectID'])) $markdown['attrs']['objectID'] = $target->id;
  756. if(!isset($markdown['attrs']['objectKey'])) $markdown['attrs']['objectKey'] = $type . '-' . $target->id;
  757. return $markdown;
  758. }
  759. /**
  760. * 将通用对象转换为 Markdown。
  761. * Convert generic object to Markdown format.
  762. *
  763. * @access public
  764. * @param string $type
  765. * @param object $target
  766. * @return array
  767. */
  768. public static function convertGenericToMarkdown($type, $target)
  769. {
  770. global $app;
  771. $typeName = zget($app->lang->zai->syncingTypeList, $type, ucfirst($type));
  772. $title = '';
  773. if(isset($target->title) && $target->title !== '') $title = $target->title;
  774. elseif(isset($target->name) && $target->name !== '') $title = $target->name;
  775. $id = isset($target->id) ? $target->id : 0;
  776. $objectTitle = trim("$typeName #$id $title");
  777. return array(
  778. 'id' => $id,
  779. 'title' => $objectTitle,
  780. 'content' => json_encode($target, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  781. 'attrs' => array('objectTitle' => $objectTitle)
  782. );
  783. }
  784. /**
  785. * 获取字段标签。
  786. *
  787. * @param array $langData
  788. * @param string $field
  789. * @return string
  790. */
  791. public static function getFieldLabel($langData, $field)
  792. {
  793. return isset($langData['fields'][$field]) ? $langData['fields'][$field] : '';
  794. }
  795. /**
  796. * 获取章节标签。
  797. *
  798. * @param array $langData
  799. * @param string $section
  800. * @return string
  801. */
  802. public static function getSectionLabel($langData, $section)
  803. {
  804. return isset($langData['sections'][$section]) ? $langData['sections'][$section] : '';
  805. }
  806. /**
  807. * 格式化字段值。
  808. *
  809. * @param array $langData
  810. * @param string $field
  811. * @param mixed $value
  812. * @return string
  813. */
  814. public static function formatFieldValue($langData, $field, $value)
  815. {
  816. if($value === null) return '';
  817. if(is_array($value))
  818. {
  819. $parts = array();
  820. foreach($value as $item)
  821. {
  822. $formatted = static::formatFieldValue($langData, $field, $item);
  823. if($formatted !== '') $parts[] = $formatted;
  824. }
  825. return $parts ? implode(',', $parts) : '';
  826. }
  827. if(is_object($value))
  828. {
  829. $value = json_decode(json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
  830. return static::formatFieldValue($langData, $field, $value);
  831. }
  832. $valueString = (string)$value;
  833. if($valueString === '') return '';
  834. if(isset($langData['maps'][$field]) && isset($langData['maps'][$field][$valueString]))
  835. {
  836. return (string)$langData['maps'][$field][$valueString];
  837. }
  838. return $valueString;
  839. }
  840. /**
  841. * 附加字段列表。
  842. *
  843. * @param array $content
  844. * @param array $langData
  845. * @param array $fieldPairs
  846. * @return void
  847. */
  848. public static function appendFieldList(&$content, $langData, $fieldPairs)
  849. {
  850. foreach($fieldPairs as $field => $value)
  851. {
  852. $label = static::getFieldLabel($langData, $field);
  853. if($label === '') continue;
  854. $formatted = static::formatFieldValue($langData, $field, $value);
  855. $content[] = "* {$label}: {$formatted}";
  856. }
  857. }
  858. /**
  859. * 附加详情章节。
  860. *
  861. * @param array $content
  862. * @param array $langData
  863. * @param string $sectionKey
  864. * @param mixed $rawValue
  865. * @return void
  866. */
  867. public static function appendDetailSection(&$content, $langData, $sectionKey, $rawValue)
  868. {
  869. if($rawValue === null) return;
  870. if(is_array($rawValue) || is_object($rawValue))
  871. {
  872. $rawValue = json_encode($rawValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  873. }
  874. $text = trim((string)$rawValue);
  875. $plain = $text === '' ? '' : trim(strip_tags($text));
  876. $label = static::getSectionLabel($langData, $sectionKey);
  877. if($label === '') $label = static::getFieldLabel($langData, $sectionKey);
  878. if($label === '') return;
  879. $content[] = "\n## {$label}\n\n" . $plain;
  880. }
  881. /**
  882. * 附加里程碑章节。
  883. *
  884. * @param array $content
  885. * @param array $langData
  886. * @param mixed $milestones
  887. * @return void
  888. */
  889. public static function appendMilestoneSection(&$content, $langData, $milestones)
  890. {
  891. if(empty($milestones)) return;
  892. $label = static::getSectionLabel($langData, 'milestone');
  893. if($label === '') $label = static::getFieldLabel($langData, 'milestone');
  894. if($label === '') return;
  895. $content[] = "\n## {$label}";
  896. foreach((array)$milestones as $milestone)
  897. {
  898. if(is_object($milestone)) $milestone = (array)$milestone;
  899. $name = isset($milestone['name']) ? trim((string)$milestone['name']) : '';
  900. $date = isset($milestone['date']) ? trim((string)$milestone['date']) : '';
  901. $line = $name;
  902. if($date !== '') $line = $line === '' ? $date : "{$line} ({$date})";
  903. if($line !== '') $content[] = "- {$line}";
  904. }
  905. }
  906. /**
  907. * 构建用例步骤文本。
  908. * Build test case steps markdown text.
  909. *
  910. * @param mixed $steps
  911. * @param array $langData
  912. * @return string
  913. */
  914. public static function buildCaseStepsText($steps, $langData)
  915. {
  916. if(empty($steps)) return '';
  917. if(is_string($steps)) return trim($steps);
  918. if(is_object($steps)) $steps = json_decode(json_encode($steps, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
  919. if(!is_array($steps)) return '';
  920. $lines = array();
  921. $index = 1;
  922. $expectLabel = static::getFieldLabel($langData, 'expect');
  923. if($expectLabel === '') $expectLabel = static::getFieldLabel($langData, 'expects');
  924. foreach($steps as $step)
  925. {
  926. if(is_object($step)) $step = (array)$step;
  927. if(!is_array($step)) continue;
  928. $desc = trim((string)($step['desc'] ?? $step['title'] ?? $step['step'] ?? ''));
  929. $expect = trim((string)($step['expect'] ?? $step['expects'] ?? ''));
  930. $line = $desc !== '' ? "{$index}. {$desc}" : "{$index}.";
  931. if($expect !== '')
  932. {
  933. $label = $expectLabel !== '' ? $expectLabel : 'Expect';
  934. $line .= " ({$label}: {$expect})";
  935. }
  936. $lines[] = $line;
  937. $index++;
  938. }
  939. return $lines ? implode("\n", $lines) : '';
  940. }
  941. /**
  942. * 收集需要展示的字段列表。
  943. * Collect field-value pairs for markdown listing.
  944. *
  945. * @param string $objectType
  946. * @param array $langData
  947. * @param object $target
  948. * @return array
  949. */
  950. public static function collectFieldPairs($objectType, $langData, $target)
  951. {
  952. if(empty($langData['fields']) || !is_array($langData['fields'])) return array();
  953. $pairs = array();
  954. $skipFields = array('desc', 'prevention', 'remedy', 'resolution', 'history', 'resolutionComment', 'steps', 'precondition', 'expect');
  955. foreach($langData['fields'] as $fieldKey => $label)
  956. {
  957. if($fieldKey === 'actions') continue;
  958. if(in_array($fieldKey, $skipFields, true)) continue;
  959. $pairs[$fieldKey] = static::extractFieldValue($objectType, $fieldKey, $target);
  960. }
  961. return $pairs;
  962. }
  963. /**
  964. * 获取指定字段的值。
  965. * Get field value from target with alias support.
  966. *
  967. * @param string $objectType
  968. * @param string $field
  969. * @param object $target
  970. * @return mixed
  971. */
  972. public static function extractFieldValue($objectType, $field, $target)
  973. {
  974. $data = (array)$target;
  975. if(array_key_exists($field, $data)) return $data[$field];
  976. $aliases = static::getFieldAliasMap($objectType);
  977. if(isset($aliases[$field]))
  978. {
  979. foreach((array)$aliases[$field] as $alias)
  980. {
  981. if(array_key_exists($alias, $data)) return $data[$alias];
  982. }
  983. }
  984. $snake = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $field));
  985. if($snake !== $field && array_key_exists($snake, $data)) return $data[$snake];
  986. if(array_key_exists($field . 'Text', $data)) return $data[$field . 'Text'];
  987. return null;
  988. }
  989. /**
  990. * 获取字段别名映射。
  991. * Get alias map for field values.
  992. *
  993. * @param string $objectType
  994. * @return array
  995. */
  996. public static function getFieldAliasMap($objectType)
  997. {
  998. static $aliasMap = array(
  999. 'issue' => array(
  1000. 'assetCreatedBy' => array('assetCreatedBy', 'createdBy', 'openedBy'),
  1001. 'assetCreatedDate' => array('assetCreatedDate', 'createdDate', 'openedDate'),
  1002. 'issueType' => array('issueType', 'type'),
  1003. 'execution' => array('execution', 'executionName'),
  1004. 'project' => array('project', 'projectName'),
  1005. 'resolution' => array('resolution', 'resolutionType'),
  1006. 'deadline' => array('deadline', 'deadLine'),
  1007. ),
  1008. 'risk' => array(
  1009. 'assetCreatedBy' => array('assetCreatedBy', 'createdBy', 'openedBy'),
  1010. 'assetCreatedDate' => array('assetCreatedDate', 'createdDate', 'openedDate'),
  1011. 'project' => array('project', 'projectName'),
  1012. 'execution' => array('execution', 'executionName'),
  1013. ),
  1014. 'opportunity' => array(
  1015. 'assetCreatedBy' => array('assetCreatedBy', 'createdBy', 'openedBy'),
  1016. 'assetCreatedDate' => array('assetCreatedDate', 'createdDate', 'openedDate'),
  1017. 'project' => array('project', 'projectName'),
  1018. 'opportunityType' => array('opportunityType', 'type'),
  1019. ),
  1020. 'plan' => array(
  1021. 'begin' => array('begin', 'start', 'beginDate'),
  1022. 'end' => array('end', 'finish', 'endDate'),
  1023. 'owner' => array('owner', 'assignedTo'),
  1024. 'stories' => array('stories', 'storyCount', 'story'),
  1025. 'bugs' => array('bugs', 'bugCount', 'bug'),
  1026. 'project' => array('project', 'projectName'),
  1027. 'product' => array('product', 'productName'),
  1028. ),
  1029. 'release' => array(
  1030. 'system' => array('system', 'systemName'),
  1031. 'project' => array('project', 'projectName'),
  1032. 'build' => array('build', 'buildName'),
  1033. 'releasedDate' => array('releasedDate', 'releaseDate'),
  1034. ),
  1035. 'ticket' => array(
  1036. 'openedBy' => array('openedBy', 'createdBy'),
  1037. 'openedDate' => array('openedDate', 'createdDate'),
  1038. 'subStatus' => array('subStatus'),
  1039. 'closedReason' => array('closedReason'),
  1040. 'project' => array('project', 'projectName'),
  1041. 'product' => array('product', 'productName'),
  1042. 'ticketType' => array('ticketType', 'type'),
  1043. ),
  1044. 'feedback' => array(
  1045. 'feedbackType' => array('feedbackType', 'type'),
  1046. ),
  1047. 'case' => array(
  1048. 'execution' => array('execution', 'executionName'),
  1049. 'module' => array('module', 'moduleName'),
  1050. 'story' => array('story', 'storyID'),
  1051. 'lastEditedBy' => array('lastEditedBy', 'editedBy'),
  1052. 'lastEditedDate'=> array('lastEditedDate', 'editedDate'),
  1053. ),
  1054. 'story' => array(
  1055. 'plan' => array('plan', 'planTitle'),
  1056. 'planTitle' => array('planTitle', 'plan'),
  1057. 'openedBy' => array('openedBy', 'createdBy'),
  1058. 'openedDate' => array('openedDate', 'createdDate'),
  1059. 'assignedTo' => array('assignedTo', 'owner'),
  1060. 'branch' => array('branch', 'branchName'),
  1061. 'module' => array('module', 'moduleName'),
  1062. 'stage' => array('stage', 'statusStage'),
  1063. ),
  1064. 'bug' => array(
  1065. 'openedBy' => array('openedBy', 'createdBy'),
  1066. 'openedDate' => array('openedDate', 'createdDate'),
  1067. 'resolvedBy' => array('resolvedBy'),
  1068. 'resolvedDate' => array('resolvedDate'),
  1069. 'assignedTo' => array('assignedTo'),
  1070. ),
  1071. );
  1072. return $aliasMap[$objectType] ?? array();
  1073. }
  1074. /**
  1075. * 获取用户字段映射。
  1076. * Get user fields map for object type.
  1077. *
  1078. * @param string $objectType
  1079. * @return array
  1080. */
  1081. public static function getUserFieldsMap($objectType)
  1082. {
  1083. $map = array(
  1084. 'story' => array('openedBy', 'assignedTo', 'reviewedBy', 'stagedBy'),
  1085. 'bug' => array('openedBy', 'assignedTo', 'resolvedBy', 'closedBy', 'feedbackBy'),
  1086. 'task' => array('openedBy', 'assignedTo', 'finishedBy', 'canceledBy', 'closedBy'),
  1087. 'feedback' => array('openedBy', 'assignedTo', 'reviewedBy', 'closedBy', 'processedBy', 'feedbackBy'),
  1088. 'ticket' => array('openedBy', 'assignedTo', 'startedBy', 'closedBy'),
  1089. 'issue' => array('assetCreatedBy', 'assignedTo'),
  1090. 'risk' => array('assetCreatedBy', 'assignedTo'),
  1091. 'opportunity'=> array('assetCreatedBy', 'assignedTo'),
  1092. 'plan' => array('owner'),
  1093. 'case' => array('openedBy', 'lastEditedBy', 'lastRunner'),
  1094. 'release' => array('owner'),
  1095. 'demand' => array('createdBy', 'assignedTo'),
  1096. 'design' => array('createdBy', 'assignedTo'),
  1097. 'doc' => array('owner', 'addedBy', 'editedBy'),
  1098. );
  1099. return $map[$objectType] ?? array();
  1100. }
  1101. /**
  1102. * 转换用户字段为真实姓名。
  1103. * Convert user account fields to realname.
  1104. *
  1105. * @param string $objectType
  1106. * @param object $target
  1107. * @return object
  1108. */
  1109. public static function convertUserFieldToRealname($objectType, $target)
  1110. {
  1111. $userFields = static::getUserFieldsMap($objectType);
  1112. if(empty($userFields)) return $target;
  1113. global $app;
  1114. $userModel = $app->loadTarget('user', '', 'model');
  1115. $userAccounts = array();
  1116. foreach($userFields as $field)
  1117. {
  1118. $value = static::extractFieldValue($objectType, $field, $target);
  1119. if(!empty($value))
  1120. {
  1121. $accounts = is_array($value) ? $value : explode(',', trim($value));
  1122. foreach($accounts as $account)
  1123. {
  1124. $account = trim($account);
  1125. if($account && $account !== '') $userAccounts[$account] = $account;
  1126. }
  1127. }
  1128. }
  1129. if(empty($userAccounts)) return $target;
  1130. $userPairs = $userModel->getPairs('realname,noletter', '', 0, array_keys($userAccounts));
  1131. $converted = clone $target;
  1132. foreach($userFields as $field)
  1133. {
  1134. $value = static::extractFieldValue($objectType, $field, $target);
  1135. if(empty($value)) continue;
  1136. $accounts = is_array($value) ? $value : explode(',', trim($value));
  1137. $realnames = array();
  1138. foreach($accounts as $account)
  1139. {
  1140. $account = trim($account);
  1141. if($account && isset($userPairs[$account]))
  1142. {
  1143. $realnames[] = $userPairs[$account];
  1144. }
  1145. elseif($account)
  1146. {
  1147. $realnames[] = $account;
  1148. }
  1149. }
  1150. if(!empty($realnames)) $converted->$field = implode(', ', $realnames);
  1151. }
  1152. return $converted;
  1153. }
  1154. /**
  1155. * 将 STORY 对象转换为 Markdown 格式。
  1156. * Convert story object to Markdown format.
  1157. *
  1158. * @access public
  1159. * @param object $story
  1160. * @return array
  1161. * @param mixed[] $langData
  1162. */
  1163. public static function convertStoryToMarkdown($story, $langData = [])
  1164. {
  1165. global $app;
  1166. $story = static::convertUserFieldToRealname('story', $story);
  1167. $app->loadLang('story');
  1168. $lang = $app->lang;
  1169. $spec = $app->dao->select('title,spec,verify,files,docs,docVersions')->from(TABLE_STORYSPEC)->where('story')->eq($story->id)->andWhere('version')->eq($story->version)->fetch();
  1170. if(empty($spec)) $spec = (object)array('title' => $story->title, 'spec' => '', 'verify' => '', 'files' => '', 'docs' => '', 'docVersions' => '');
  1171. $planValue = $story->plan;
  1172. if(!empty($planValue))
  1173. {
  1174. $planIDs = explode(',', trim($planValue));
  1175. $planNames = array();
  1176. foreach($planIDs as $planID)
  1177. {
  1178. $planID = trim($planID);
  1179. if(empty($planID) || !is_numeric($planID)) continue;
  1180. $plan = $app->dao->select('title')->from(TABLE_PRODUCTPLAN)->where('id')->eq($planID)->fetch();
  1181. if($plan && !empty($plan->title))
  1182. {
  1183. $planNames[] = $plan->title;
  1184. }
  1185. else
  1186. {
  1187. $planNames[] = $planID;
  1188. }
  1189. }
  1190. if(!empty($planNames))
  1191. {
  1192. $planValue = implode(', ', $planNames);
  1193. }
  1194. }
  1195. $content = array();
  1196. $content[] = "# {$lang->SRCommon} #$story->id $story->title\n";
  1197. $content[] = "## {$lang->story->legendBasicInfo}\n";
  1198. $content[] = "* {$lang->story->status}: " . zget($lang->story->statusList, $story->status);
  1199. $content[] = "* {$lang->story->stage}: " . zget($lang->story->stageList, $story->stage);
  1200. $content[] = "* {$lang->story->pri}: " . zget($lang->story->priList, $story->pri);
  1201. $content[] = "* {$lang->story->version}: $story->version";
  1202. $content[] = "* {$lang->story->category}: " . zget($lang->story->categoryList, $story->category);
  1203. $content[] = "* {$lang->story->source}: " . zget($lang->story->sourceList, $story->source);
  1204. $content[] = "* {$lang->story->estimate}: $story->estimate";
  1205. $content[] = "* {$lang->story->product}: $story->product";
  1206. $content[] = "* {$lang->story->plan}: $planValue";
  1207. $content[] = "* {$lang->story->branch}: $story->branch";
  1208. $content[] = "* {$lang->story->parent}: " . (is_array($story->parent) ? implode(',', $story->parent) : $story->parent);
  1209. $content[] = "* {$lang->story->module}: $story->module";
  1210. $content[] = "* {$lang->story->keywords}: $story->keywords";
  1211. $content[] = "* {$lang->story->assign}: $story->assignedTo";
  1212. $content[] = "* {$lang->story->assignedDate}: $story->assignedDate";
  1213. $content[] = "* {$lang->story->reviewedDate}: $story->reviewedDate";
  1214. $content[] = "* {$lang->story->reviewedBy}: $story->reviewedBy";
  1215. $content[] = "* {$lang->story->openedBy}: $story->openedBy";
  1216. $content[] = "* {$lang->story->openedDate}: $story->openedDate";
  1217. $content[] = "* {$lang->story->stagedBy}: $story->stagedBy";
  1218. $content[] = "\n## {$lang->story->spec}\n";
  1219. $content[] = strip_tags($spec->spec) . "\n";
  1220. $content[] = "## {$lang->story->verify}\n";
  1221. $content[] = strip_tags($spec->verify) . "\n";
  1222. $markdown = array('id' => $story->id, 'title' => "$lang->SRCommon #$story->id $spec->title");
  1223. $markdown['content'] = implode("\n", $content);
  1224. $markdown['attrs'] = array(
  1225. 'product' => $story->product,
  1226. 'parentStory' => $story->parent,
  1227. 'productModule' => $story->module,
  1228. 'productBranch' => $story->branch,
  1229. 'productPlan' => $planValue,
  1230. 'status' => $story->status,
  1231. 'stage' => $story->stage
  1232. );
  1233. if(isset($story->lib)) $markdown['attrs']['lib'] = $story->lib;
  1234. return $markdown;
  1235. }
  1236. /**
  1237. * 将 CASE 对象转换为 Markdown 格式。
  1238. * Convert case object to Markdown format.
  1239. *
  1240. * @access public
  1241. * @param object $case
  1242. * @param array $langData
  1243. * @return array
  1244. */
  1245. public static function convertCaseToMarkdown($case, $langData = [])
  1246. {
  1247. $case = static::convertUserFieldToRealname('case', $case);
  1248. $id = $case->id ?? 0;
  1249. $title = trim((string)($case->title ?? $case->name ?? ''));
  1250. $typeName = isset($langData['common']) ? $langData['common'] : '';
  1251. $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title");
  1252. if($header === '') $header = "#$id $title";
  1253. $markdown = array('id' => $id, 'title' => $header);
  1254. $content = array();
  1255. $content[] = "# {$header}\n";
  1256. $sectionBasic = static::getSectionLabel($langData, 'basic');
  1257. if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n";
  1258. $fieldPairs = static::collectFieldPairs('case', $langData, $case);
  1259. static::appendFieldList($content, $langData, $fieldPairs);
  1260. static::appendDetailSection($content, $langData, 'precondition', $case->precondition ?? null);
  1261. $stepsText = static::buildCaseStepsText($case->steps ?? null, $langData);
  1262. if($stepsText !== '') static::appendDetailSection($content, $langData, 'steps', $stepsText);
  1263. static::appendDetailSection($content, $langData, 'expect', $case->expect ?? ($case->expects ?? null));
  1264. $markdown['content'] = implode("\n", $content);
  1265. $markdown['attrs'] = array(
  1266. 'product' => $case->product ?? '',
  1267. 'module' => $case->module ?? '',
  1268. 'project' => $case->project ?? '',
  1269. 'execution' => $case->execution ?? '',
  1270. 'branch' => $case->branch ?? '',
  1271. 'path' => $case->path ?? '',
  1272. 'story' => $case->story ?? '',
  1273. 'storyVersion' => $case->storyVersion ?? '',
  1274. 'pri' => $case->pri ?? '',
  1275. 'type' => $case->type ?? '',
  1276. 'status' => $case->status ?? '',
  1277. 'stage' => $case->stage ?? '',
  1278. 'lib' => $case->lib ?? ''
  1279. );
  1280. return $markdown;
  1281. }
  1282. /**
  1283. * 将 DEMAND 对象转换为 Markdown 格式。
  1284. * Convert demand object to Markdown format.
  1285. *
  1286. * @access public
  1287. * @param object $demand
  1288. * @return array
  1289. * @param mixed[] $langData
  1290. */
  1291. public static function convertDemandToMarkdown($demand, $langData = [])
  1292. {
  1293. global $app;
  1294. $demand = static::convertUserFieldToRealname('demand', $demand);
  1295. $app->loadLang('demand');
  1296. $lang = $app->lang;
  1297. $spec = $app->dao->select('title,spec,verify')->from(TABLE_DEMANDSPEC)->where('demand')->eq($demand->id)->andWhere('version')->eq($demand->version)->fetch();
  1298. if(empty($spec)) $spec = (object)array('title' => $demand->title, 'spec' => '', 'verify' => '');
  1299. $markdown = array('id' => $demand->id, 'title' => "$lang->SRCommon #$demand->id $spec->title");
  1300. $content = array();
  1301. $content[] = "# {$lang->demand->common} #$demand->id $demand->title\n";
  1302. $content[] = "## {$lang->demand->legendBasicInfo}\n";
  1303. $content[] = "* {$lang->demand->status}: " . zget($lang->demand->statusList, $demand->status);
  1304. $content[] = "* {$lang->demand->stage}: " . zget($lang->demand->stageList, $demand->stage);
  1305. $content[] = "* {$lang->demand->pri}: " . zget($lang->demand->priList, $demand->pri);
  1306. $content[] = "* {$lang->demand->version}: $demand->version";
  1307. $content[] = "* {$lang->demand->category}: " . zget($lang->demand->categoryList, $demand->category);
  1308. $content[] = "* {$lang->demand->source}: " . zget($lang->demand->sourceList, $demand->source);
  1309. $content[] = "* {$lang->demand->product}: $demand->product";
  1310. $content[] = "* {$lang->demand->parent}: $demand->parent";
  1311. $content[] = "* {$lang->demand->module}: $demand->module";
  1312. $content[] = "* {$lang->demand->keywords}: $demand->keywords";
  1313. $content[] = "* {$lang->demand->assignedTo}: $demand->assignedTo";
  1314. $content[] = "* {$lang->demand->assignedDate}: $demand->assignedDate";
  1315. $content[] = "* {$lang->demand->createdBy}: $demand->createdBy";
  1316. $content[] = "* {$lang->demand->createdDate}: $demand->createdDate";
  1317. $content[] = "* {$lang->demand->changedBy}: $demand->changedBy";
  1318. $content[] = "* {$lang->demand->changedDate}: $demand->changedDate";
  1319. $content[] = "* {$lang->demand->closedBy}: $demand->closedBy";
  1320. $content[] = "* {$lang->demand->closedDate}: $demand->closedDate";
  1321. $content[] = "* {$lang->demand->closedReason}: $demand->closedReason";
  1322. $content[] = "* {$lang->demand->submitedBy}: $demand->submitedBy";
  1323. $content[] = "* {$lang->demand->distributedBy}: $demand->distributedBy";
  1324. $content[] = "* {$lang->demand->distributedDate}: $demand->distributedDate";
  1325. $content[] = "## {$lang->demand->spec}\n";
  1326. $content[] = strip_tags($spec->spec) . "\n";
  1327. $content[] = "## {$lang->demand->verify}\n";
  1328. $content[] = strip_tags($spec->verify) . "\n";
  1329. $markdown['content'] = implode("\n", $content);
  1330. $markdown['attrs'] = array('product' => $demand->product, 'parentDemand' => $demand->parent, 'productModule' => $demand->module, 'status' => $demand->status, 'stage' => $demand->stage);
  1331. return $markdown;
  1332. }
  1333. /**
  1334. * 将 BUG 对象转换为 Markdown 格式。
  1335. * Convert bug object to markdown format.
  1336. *
  1337. * @access public
  1338. * @param object $bug
  1339. * @return array
  1340. * @param mixed[] $langData
  1341. */
  1342. public static function convertBugToMarkdown($bug, $langData = [])
  1343. {
  1344. global $app;
  1345. $bug = static::convertUserFieldToRealname('bug', $bug);
  1346. $app->loadLang('bug');
  1347. $lang = $app->lang;
  1348. $markdown = array('id' => $bug->id, 'title' => "{$lang->bug->common} #$bug->id $bug->title");
  1349. $content = array();
  1350. $content[] = "# {$lang->bug->common} #$bug->id $bug->title\n";
  1351. $content[] = "## {$lang->bug->legendBasicInfo}\n";
  1352. $content[] = "* {$lang->bug->pri}: " . zget($lang->bug->priList, $bug->pri);
  1353. $content[] = "* {$lang->bug->severity}: " . zget($lang->bug->severityList, $bug->severity);
  1354. $content[] = "* {$lang->bug->status}: " . zget($lang->bug->statusList, $bug->status);
  1355. $content[] = "* {$lang->bug->resolution}: " . zget($lang->bug->resolutionList, $bug->resolution);
  1356. $content[] = "* {$lang->bug->type}: " . zget($lang->bug->typeList, $bug->type);
  1357. $content[] = "* {$lang->bug->product}: $bug->product";
  1358. $content[] = "* {$lang->bug->project}: $bug->project";
  1359. $content[] = "* {$lang->bug->execution}: $bug->execution";
  1360. $content[] = "* {$lang->bug->module}: $bug->module";
  1361. $content[] = "* {$lang->bug->branch}: $bug->branch";
  1362. $content[] = "* {$lang->bug->plan}: $bug->plan";
  1363. $content[] = "* {$lang->bug->story}: $bug->story";
  1364. $content[] = "* {$lang->bug->relatedBug}: $bug->relatedBug";
  1365. $content[] = "* {$lang->bug->keywords}: $bug->keywords";
  1366. $content[] = "* {$lang->bug->resolvedBy}: $bug->resolvedBy";
  1367. $content[] = "* {$lang->bug->resolvedDate}: $bug->resolvedDate";
  1368. $content[] = "* {$lang->bug->resolvedBuild}: $bug->resolvedBuild";
  1369. $content[] = "* {$lang->bug->openedBy}: $bug->openedBy";
  1370. $content[] = "* {$lang->bug->openedDate}: $bug->openedDate";
  1371. $content[] = "* {$lang->bug->openedBuild}: $bug->openedBuild";
  1372. $content[] = "* {$lang->bug->assignedTo}: $bug->assignedTo";
  1373. $content[] = "* {$lang->bug->assignedDate}: $bug->assignedDate";
  1374. $content[] = "* {$lang->bug->closedBy}: $bug->closedBy";
  1375. $content[] = "* {$lang->bug->closedDate}: $bug->closedDate";
  1376. $content[] = "* {$lang->bug->feedbackBy}: $bug->feedbackBy";
  1377. $content[] = "* {$lang->bug->activatedDate}: $bug->activatedDate";
  1378. $content[] = "\n## {$lang->bug->steps}\n";
  1379. $content[] = strip_tags($bug->steps) . "\n";
  1380. $markdown['content'] = implode("\n", $content);
  1381. $markdown['attrs'] = array('product' => $bug->product, 'module' => $bug->module, 'branch' => $bug->branch, 'plan' => $bug->plan, 'relatedBug' => $bug->relatedBug, 'story' => $bug->story, 'task' => $bug->task);
  1382. return $markdown;
  1383. }
  1384. /**
  1385. * 将任务对象转换为 Markdown 格式。
  1386. * Convert task object to markdown format.
  1387. *
  1388. * @param object $task
  1389. * @access public
  1390. * @return array
  1391. * @param mixed[] $langData
  1392. */
  1393. public static function convertTaskToMarkdown($task, $langData = [])
  1394. {
  1395. global $app;
  1396. $task = static::convertUserFieldToRealname('task', $task);
  1397. $app->loadLang('task');
  1398. $lang = $app->lang;
  1399. $markdown = array('id' => $task->id, 'title' => "{$lang->task->common} #$task->id $task->name");
  1400. $content = array();
  1401. $content[] = "# {$lang->task->common} #$task->id $task->name\n";
  1402. $content[] = "## {$lang->task->legendBasic}\n";
  1403. $content[] = "* {$lang->task->project}: $task->project";
  1404. $content[] = "* {$lang->task->execution}: $task->execution";
  1405. $content[] = "* {$lang->task->module}: $task->module";
  1406. $content[] = "* {$lang->task->fromBug}: $task->fromBug";
  1407. $content[] = "* {$lang->task->feedback}: $task->feedback";
  1408. $content[] = "* {$lang->task->story}: $task->storyID";
  1409. $content[] = "* {$lang->task->assignedTo}: $task->assignedTo";
  1410. $content[] = "* {$lang->task->assignedDate}: $task->assignedDate";
  1411. $content[] = "* {$lang->task->type}: " . zget($lang->task->typeList, $task->type);
  1412. $content[] = "* {$lang->task->status}: " . zget($lang->task->statusList, $task->status);
  1413. $content[] = "* {$lang->task->pri}: " . zget($lang->task->priList, $task->pri);
  1414. $content[] = "* {$lang->task->keywords}: $task->keywords";
  1415. $content[] = "* {$lang->task->mailto}: $task->mailto";
  1416. $content[] = "## {$lang->task->legendEffort}\n";
  1417. $content[] = "* {$lang->task->estimate}: $task->estimate";
  1418. $content[] = "* {$lang->task->consumed}: $task->consumed";
  1419. $content[] = "* {$lang->task->left}: $task->left";
  1420. $content[] = "* {$lang->task->estStarted}: $task->estStarted";
  1421. $content[] = "* {$lang->task->realStarted}: $task->realStarted";
  1422. $content[] = "* {$lang->task->deadline}: $task->deadline";
  1423. $content[] = "## {$lang->task->legendLife}\n";
  1424. $content[] = "* {$lang->task->openedBy}: $task->openedBy";
  1425. $content[] = "* {$lang->task->openedDate}: $task->openedDate";
  1426. $content[] = "* {$lang->task->finishedBy}: $task->finishedBy";
  1427. $content[] = "* {$lang->task->finishedDate}: $task->finishedDate";
  1428. $content[] = "* {$lang->task->canceledBy}: $task->canceledBy";
  1429. $content[] = "* {$lang->task->canceledDate}: $task->canceledDate";
  1430. $content[] = "* {$lang->task->closedBy}: $task->closedBy";
  1431. $content[] = "* {$lang->task->closedDate}: $task->closedDate";
  1432. $content[] = "* {$lang->task->closedReason}: $task->closedReason";
  1433. $content[] = "\n## {$lang->task->desc}\n";
  1434. $content[] = strip_tags($task->desc) . "\n";
  1435. $markdown['content'] = implode("\n", $content);
  1436. $markdown['attrs'] = array('project' => $task->project, 'module' => $task->module, 'design' => $task->design, 'fromBug' => $task->fromBug, 'story' => $task->storyID);
  1437. return $markdown;
  1438. }
  1439. /**
  1440. * 将 DOC 对象转换为 Markdown 格式。
  1441. * Convert doc object to markdown format.
  1442. *
  1443. * @access public
  1444. * @param object $doc
  1445. * @return array
  1446. * @param mixed[] $langData
  1447. */
  1448. public static function convertDocToMarkdown($doc, $langData = [])
  1449. {
  1450. global $app;
  1451. $doc = static::convertUserFieldToRealname('doc', $doc);
  1452. $app->loadLang('doc');
  1453. $lang = $app->lang;
  1454. if(isset($doc->protocol) || !empty($doc->api))
  1455. {
  1456. $app->loadLang('api');
  1457. $markdown = array('id' => $doc->id, 'title' => "{$lang->doc->common} #$doc->id $doc->title");
  1458. $content = array();
  1459. $content[] = "# {$app->lang->api->common} #$doc->id $doc->title\n";
  1460. $content[] = "## {$lang->doc->basicInfo}\n";
  1461. $content[] = "* {$lang->doc->product}: $doc->product";
  1462. $content[] = "* {$lang->doc->lib}: $doc->lib";
  1463. $content[] = "* {$app->lang->api->module}: $doc->module";
  1464. $content[] = "* {$app->lang->api->title}: $doc->title";
  1465. $content[] = "* {$app->lang->api->path}: $doc->path";
  1466. $content[] = "* {$app->lang->api->protocol}: $doc->protocol";
  1467. $content[] = "* {$app->lang->api->method}: $doc->method";
  1468. $content[] = "* {$app->lang->api->requestType}: $doc->requestType";
  1469. $content[] = "* {$app->lang->api->status}: " . zget($app->lang->api->statusOptions, $doc->status);
  1470. $content[] = "* {$app->lang->api->owner}: $doc->owner";
  1471. $content[] = "* {$app->lang->api->version}: $doc->version";
  1472. if(!empty($doc->params->header))
  1473. {
  1474. $content[] = "\n## {$app->lang->api->header}\n";
  1475. $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n";
  1476. $content[] = "|------|---|---------|\n";
  1477. foreach($doc->params->header as $item)
  1478. {
  1479. if(is_array($item)) $item = (object)$item;
  1480. if(!is_object($item)) continue;
  1481. $desc = strip_tags($item->desc);
  1482. $required = zget($lang->api->boolList, $item->required);
  1483. $content[] = "| {$item->field} | $required | $desc |\n";
  1484. }
  1485. }
  1486. if(!empty($doc->params->query))
  1487. {
  1488. $content[] = "\n## {$app->lang->api->query}\n";
  1489. $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n";
  1490. $content[] = "|------|---|---------|\n";
  1491. foreach($doc->params->query as $item)
  1492. {
  1493. if(is_array($item)) $item = (object)$item;
  1494. if(!is_object($item)) continue;
  1495. $desc = strip_tags($item->desc);
  1496. $required = zget($lang->api->boolList, $item->required);
  1497. $content[] = "| {$item->field} | $required | $desc |\n";
  1498. }
  1499. }
  1500. if(!empty($doc->params->params))
  1501. {
  1502. $content[] = "\n## {$app->lang->api->params}\n";
  1503. $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->type} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n";
  1504. $content[] = "|------|---|---|---------|\n";
  1505. foreach($doc->params->params as $item)
  1506. {
  1507. if(is_array($item)) $item = (object)$item;
  1508. if(!is_object($item)) continue;
  1509. $desc = strip_tags($item->desc);
  1510. $required = zget($lang->api->boolList, $item->required);
  1511. $content[] = "| {$item->field} | {$item->paramsType} | $required | $desc |\n";
  1512. }
  1513. }
  1514. if(!empty($doc->paramsExample))
  1515. {
  1516. $content[] = "\n## {$app->lang->api->paramsExample}\n";
  1517. $content[] = "```json\n" . htmlspecialchars_decode($doc->paramsExample) . "\n```";
  1518. }
  1519. if(!empty($doc->response))
  1520. {
  1521. $content[] = "\n## {$app->lang->api->response}\n";
  1522. $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n";
  1523. $content[] = "|------|---|---------|\n";
  1524. foreach($doc->response as $item)
  1525. {
  1526. if(is_array($item)) $item = (object)$item;
  1527. if(!is_object($item)) continue;
  1528. $desc = strip_tags($item->desc);
  1529. $required = zget($lang->api->boolList, $item->required);
  1530. $content[] = "| {$item->field} | $required | $desc |\n";
  1531. }
  1532. }
  1533. if(!empty($doc->responseExample))
  1534. {
  1535. $content[] = "\n## {$app->lang->api->responseExample}\n";
  1536. $content[] = "```json\n" . htmlspecialchars_decode($doc->responseExample) . "\n```";
  1537. }
  1538. $content[] = "\n## {$app->lang->api->desc}\n";
  1539. $content[] = strip_tags($doc->desc) . "\n";
  1540. $markdown['content'] = implode("\n", $content);
  1541. $markdown['attrs'] = array('product' => $doc->product, 'lib' => $doc->lib, 'module' => $doc->module, 'version' => $doc->version, 'docType' => 'api');
  1542. }
  1543. else
  1544. {
  1545. $docContent = $app->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($doc->id)->andWhere('version')->eq($doc->version)->fetch();
  1546. if(empty($docContent))
  1547. {
  1548. $docContent = new stdClass();
  1549. $docContent->title = $doc->title;
  1550. $docContent->content = isset($doc->content) ? $doc->content : '';
  1551. }
  1552. $markdown = array('id' => $doc->id, 'title' => "{$lang->doc->common} #$doc->id $docContent->title");
  1553. $content = array();
  1554. $content[] = "# {$lang->doc->common} #$doc->id $docContent->title\n";
  1555. $content[] = "## {$lang->doc->basicInfo}\n";
  1556. $content[] = "* {$lang->doc->title}: $docContent->title";
  1557. $content[] = "* {$lang->doc->type}: " . zget($lang->doc->typeList, $doc->type);
  1558. $content[] = "* {$lang->doc->product}: $doc->product";
  1559. $content[] = "* {$lang->doc->project}: $doc->project";
  1560. $content[] = "* {$lang->doc->execution}: $doc->execution";
  1561. $content[] = "* {$lang->doc->version}: $doc->version";
  1562. $content[] = "* {$lang->doc->lib}: $doc->lib";
  1563. $content[] = "* {$lang->doc->module}: $doc->module";
  1564. $content[] = "\n---\n";
  1565. $content[] = strip_tags($docContent->content) . "\n";
  1566. $markdown['content'] = implode("\n", $content);
  1567. $markdown['attrs'] = array('objectTitle' => $markdown['title'], 'product' => $doc->product, 'lib' => $doc->lib, 'module' => $doc->module, 'project' => $doc->project, 'execution' => $doc->execution, 'type' => $doc->type, 'version' => $doc->version);
  1568. }
  1569. return $markdown;
  1570. }
  1571. /**
  1572. * 将 DESIGN 对象转换为 Markdown 格式。
  1573. * Convert design object to markdown format.
  1574. *
  1575. * @access public
  1576. * @param object $design
  1577. * @return array
  1578. * @param mixed[] $langData
  1579. */
  1580. public static function convertDesignToMarkdown($design, $langData = [])
  1581. {
  1582. global $app;
  1583. $design = static::convertUserFieldToRealname('design', $design);
  1584. $app->loadLang('design');
  1585. $lang = $app->lang;
  1586. $designSpec = $app->dao->select('*')->from(TABLE_DESIGNSPEC)->where('design')->eq($design->id)->andWhere('version')->eq($design->version)->fetch();
  1587. if(empty($designSpec)) $designSpec = (object)array('name' => $design->name, 'desc' => '');
  1588. $markdown = array('id' => $design->id, 'title' => "{$lang->design->common} #$design->id $designSpec->name");
  1589. $content = array();
  1590. $content[] = "# {$lang->design->common} #$design->id $designSpec->name\n";
  1591. $content[] = "## {$lang->design->basicInfo}\n";
  1592. $content[] = "* {$lang->design->type}: " . zget($lang->design->typeList, $design->type);
  1593. $content[] = "* {$lang->design->product}: $design->product";
  1594. $content[] = "* {$lang->design->project}: $design->project";
  1595. $content[] = "* {$lang->design->story}: $design->story";
  1596. $content[] = "* {$lang->design->version}: $design->version";
  1597. $content[] = "* {$lang->design->assignedTo}: $design->assignedTo";
  1598. $content[] = "* {$lang->design->createdBy}: $design->createdBy";
  1599. $content[] = "* {$lang->design->createdDate}: $design->createdDate";
  1600. $content[] = "\n## {$lang->design->desc}\n";
  1601. $content[] = strip_tags($designSpec->desc) . "\n";
  1602. $markdown['content'] = implode("\n", $content);
  1603. $markdown['attrs'] = array('objectTitle' => $markdown['title'],'product' => $design->product, 'story' => $design->story, 'project' => $design->project, 'execution' => $design->execution, 'type' => $design->type);
  1604. return $markdown;
  1605. }
  1606. /**
  1607. * 将 FEEDBACK 对象转换为 Markdown 格式。
  1608. * Convert feedback object to markdown format.
  1609. *
  1610. * @access public
  1611. * @param object $feedback
  1612. * @return array
  1613. * @param mixed[] $langData
  1614. */
  1615. public static function convertFeedbackToMarkdown($feedback, $langData = [])
  1616. {
  1617. global $app;
  1618. $feedback = static::convertUserFieldToRealname('feedback', $feedback);
  1619. $app->loadLang('feedback');
  1620. $lang = $app->lang;
  1621. $markdown = array('id' => $feedback->id, 'title' => "{$lang->feedback->common} #$feedback->id $feedback->title");
  1622. $content = array();
  1623. $feedbackType = $feedback->feedbackType ?? $feedback->type ?? '';
  1624. $content[] = "# {$lang->feedback->common} #$feedback->id $feedback->title\n";
  1625. $content[] = "## {$lang->feedback->labelBasic}\n";
  1626. $content[] = "* {$lang->feedback->feedbackBy}: $feedback->feedbackBy";
  1627. $content[] = "* {$lang->feedback->type}: " . zget($lang->feedback->typeList, $feedbackType);
  1628. $content[] = "* {$lang->feedback->pri}: " . zget($lang->feedback->priList, $feedback->pri);
  1629. $content[] = "* {$lang->feedback->status}: " . zget($lang->feedback->statusList, $feedback->status);
  1630. $content[] = "* {$lang->feedback->solution}: " . zget($lang->feedback->solutionList, $feedback->solution);
  1631. $content[] = "* {$lang->feedback->product}: $feedback->product";
  1632. $content[] = "* {$lang->feedback->module}: $feedback->module";
  1633. $content[] = "* {$lang->feedback->openedBy}: $feedback->openedBy";
  1634. $content[] = "* {$lang->feedback->openedDate}: $feedback->openedDate";
  1635. $content[] = "* {$lang->feedback->assignedTo}: $feedback->assignedTo";
  1636. $content[] = "* {$lang->feedback->assignedDate}: $feedback->assignedDate";
  1637. $content[] = "* {$lang->feedback->reviewedBy}: $feedback->reviewedBy";
  1638. $content[] = "* {$lang->feedback->reviewedDate}: $feedback->reviewedDate";
  1639. $content[] = "* {$lang->feedback->closedBy}: $feedback->closedBy";
  1640. $content[] = "* {$lang->feedback->closedDate}: $feedback->closedDate";
  1641. $content[] = "* {$lang->feedback->closedReason}: " . zget($lang->feedback->closedReasonList, $feedback->closedReason);
  1642. $content[] = "* {$lang->feedback->processedBy}: $feedback->processedBy";
  1643. $content[] = "* {$lang->feedback->processedDate}: $feedback->processedDate";
  1644. $content[] = "* {$lang->feedback->source}: $feedback->source";
  1645. $content[] = "* {$lang->feedback->result}: $feedback->result";
  1646. $content[] = "* {$lang->feedback->keywords}: $feedback->keywords";
  1647. $content[] = "* {$lang->feedback->faq}: $feedback->faq";
  1648. $content[] = "\n## {$lang->feedback->desc}\n";
  1649. $content[] = strip_tags($feedback->desc) . "\n";
  1650. $markdown['content'] = implode("\n", $content);
  1651. $markdown['attrs'] = array('objectTitle' => $markdown['title'], 'product' => $feedback->product, 'module' => $feedback->module, 'type' => $feedbackType, 'status' => $feedback->status, 'pri' => $feedback->pri);
  1652. return $markdown;
  1653. }
  1654. /**
  1655. * 将 ISSUE 对象转换为 Markdown。
  1656. * Convert issue object to Markdown format.
  1657. *
  1658. * @access public
  1659. * @param object $issue
  1660. * @param array $langData
  1661. * @return array
  1662. */
  1663. public static function convertIssueToMarkdown($issue, $langData = [])
  1664. {
  1665. $issue = static::convertUserFieldToRealname('issue', $issue);
  1666. $id = $issue->id ?? 0;
  1667. $title = trim((string)($issue->title ?? $issue->name ?? ''));
  1668. $typeName = isset($langData['common']) ? $langData['common'] : '';
  1669. $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title");
  1670. if($header === '') $header = "#$id $title";
  1671. $markdown = array('id' => $id, 'title' => $header);
  1672. $content = array();
  1673. $content[] = "# {$header}\n";
  1674. $sectionBasic = static::getSectionLabel($langData, 'basic');
  1675. if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n";
  1676. $fieldPairs = static::collectFieldPairs('issue', $langData, $issue);
  1677. static::appendFieldList($content, $langData, $fieldPairs);
  1678. static::appendDetailSection($content, $langData, 'desc', $issue->desc ?? null);
  1679. static::appendDetailSection($content, $langData, 'solution', $issue->resolutionComment ?? null);
  1680. $markdown['content'] = implode("\n", $content);
  1681. $markdown['attrs'] = array(
  1682. 'objectTitle' => $markdown['title'],
  1683. 'status' => $issue->status ?? '',
  1684. 'pri' => $issue->pri ?? '',
  1685. 'severity' => $issue->severity ?? '',
  1686. 'project' => $issue->project ?? '',
  1687. 'execution' => $issue->execution ?? '',
  1688. 'assignedTo' => $issue->assignedTo ?? '',
  1689. 'resolution' => $issue->resolution ?? '',
  1690. 'issueType' => $issue->issueType ?? ($issue->type ?? ''),
  1691. );
  1692. return $markdown;
  1693. }
  1694. /**
  1695. * 将 RISK 对象转换为 Markdown。
  1696. * Convert risk object to Markdown format.
  1697. *
  1698. * @access public
  1699. * @param object $risk
  1700. * @param array $langData
  1701. * @return array
  1702. */
  1703. public static function convertRiskToMarkdown($risk, $langData = [])
  1704. {
  1705. $risk = static::convertUserFieldToRealname('risk', $risk);
  1706. $id = $risk->id ?? 0;
  1707. $title = trim((string)($risk->name ?? ''));
  1708. $typeName = isset($langData['common']) ? $langData['common'] : '';
  1709. $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title");
  1710. if($header === '') $header = "#$id $title";
  1711. $markdown = array('id' => $id, 'title' => $header);
  1712. $content = array();
  1713. $content[] = "# {$header}\n";
  1714. $sectionBasic = static::getSectionLabel($langData, 'basic');
  1715. if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n";
  1716. $fieldPairs = static::collectFieldPairs('risk', $langData, $risk);
  1717. static::appendFieldList($content, $langData, $fieldPairs);
  1718. static::appendDetailSection($content, $langData, 'desc', $risk->desc ?? null);
  1719. static::appendDetailSection($content, $langData, 'prevention', $risk->prevention ?? null);
  1720. static::appendDetailSection($content, $langData, 'remedy', $risk->remedy ?? null);
  1721. $markdown['content'] = implode("\n", $content);
  1722. $markdown['attrs'] = array(
  1723. 'objectTitle' => $markdown['title'],
  1724. 'status' => $risk->status ?? '',
  1725. 'probability' => $risk->probability ?? ($risk->chance ?? ''),
  1726. 'impact' => $risk->impact ?? '',
  1727. 'strategy' => $risk->strategy ?? '',
  1728. 'assignedTo' => $risk->assignedTo ?? '',
  1729. 'project' => $risk->project ?? '',
  1730. 'execution' => $risk->execution ?? '',
  1731. );
  1732. return $markdown;
  1733. }
  1734. /**
  1735. * 将 OPPORTUNITY 对象转换为 Markdown。
  1736. * Convert opportunity object to Markdown format.
  1737. *
  1738. * @access public
  1739. * @param object $opportunity
  1740. * @param array $langData
  1741. * @return array
  1742. */
  1743. public static function convertOpportunityToMarkdown($opportunity, $langData = [])
  1744. {
  1745. $opportunity = static::convertUserFieldToRealname('opportunity', $opportunity);
  1746. $id = $opportunity->id ?? 0;
  1747. $title = trim((string)($opportunity->name ?? ''));
  1748. $typeName = isset($langData['common']) ? $langData['common'] : '';
  1749. $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title");
  1750. if($header === '') $header = "#$id $title";
  1751. $markdown = array('id' => $id, 'title' => $header);
  1752. $content = array();
  1753. $content[] = "# {$header}\n";
  1754. $sectionBasic = static::getSectionLabel($langData, 'basic');
  1755. if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n";
  1756. $fieldPairs = static::collectFieldPairs('opportunity', $langData, $opportunity);
  1757. static::appendFieldList($content, $langData, $fieldPairs);
  1758. static::appendDetailSection($content, $langData, 'desc', $opportunity->desc ?? null);
  1759. static::appendDetailSection($content, $langData, 'prevention', $opportunity->prevention ?? null);
  1760. $markdown['content'] = implode("\n", $content);
  1761. $markdown['attrs'] = array(
  1762. 'objectTitle' => $markdown['title'],
  1763. 'status' => $opportunity->status ?? '',
  1764. 'benefit' => $opportunity->benefit ?? ($opportunity->impact ?? ''),
  1765. 'chance' => $opportunity->chance ?? '',
  1766. 'type' => $opportunity->type ?? '',
  1767. 'strategy' => $opportunity->strategy ?? '',
  1768. 'assignedTo' => $opportunity->assignedTo ?? '',
  1769. 'project' => $opportunity->project ?? '',
  1770. );
  1771. return $markdown;
  1772. }
  1773. /**
  1774. * 将 PLAN 对象转换为 Markdown。
  1775. * Convert plan object to Markdown format.
  1776. *
  1777. * @access public
  1778. * @param object $plan
  1779. * @param array $langData
  1780. * @return array
  1781. */
  1782. public static function convertPlanToMarkdown($plan, $langData = [])
  1783. {
  1784. $plan = static::convertUserFieldToRealname('plan', $plan);
  1785. $id = $plan->id ?? 0;
  1786. $title = trim((string)($plan->title ?? $plan->name ?? ''));
  1787. $typeName = isset($langData['common']) ? $langData['common'] : '';
  1788. $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title");
  1789. if($header === '') $header = "#$id $title";
  1790. $markdown = array('id' => $id, 'title' => $header);
  1791. $content = array();
  1792. $content[] = "# {$header}\n";
  1793. $sectionBasic = static::getSectionLabel($langData, 'basic');
  1794. if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n";
  1795. $fieldPairs = static::collectFieldPairs('plan', $langData, $plan);
  1796. static::appendFieldList($content, $langData, $fieldPairs);
  1797. static::appendDetailSection($content, $langData, 'desc', $plan->desc ?? null);
  1798. static::appendMilestoneSection($content, $langData, $plan->milestones ?? null);
  1799. $markdown['content'] = implode("\n", $content);
  1800. $markdown['attrs'] = array(
  1801. 'objectTitle' => $markdown['title'],
  1802. 'status' => $plan->status ?? '',
  1803. 'product' => $plan->product ?? '',
  1804. 'project' => $plan->project ?? '',
  1805. 'begin' => $plan->begin ?? ($plan->start ?? ''),
  1806. 'end' => $plan->end ?? '',
  1807. 'owner' => $plan->owner ?? ($plan->assignedTo ?? ''),
  1808. );
  1809. return $markdown;
  1810. }
  1811. /**
  1812. * 将 RELEASE 对象转换为 Markdown。
  1813. * Convert release object to Markdown format.
  1814. *
  1815. * @access public
  1816. * @param object $release
  1817. * @param array $langData
  1818. * @return array
  1819. */
  1820. public static function convertReleaseToMarkdown($release, $langData = [])
  1821. {
  1822. $release = static::convertUserFieldToRealname('release', $release);
  1823. $id = $release->id ?? 0;
  1824. $title = trim((string)($release->name ?? ''));
  1825. $typeName = isset($langData['common']) ? $langData['common'] : '';
  1826. $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title");
  1827. if($header === '') $header = "#$id $title";
  1828. $markdown = array('id' => $id, 'title' => $header);
  1829. $content = array();
  1830. $content[] = "# {$header}\n";
  1831. $sectionBasic = static::getSectionLabel($langData, 'basic');
  1832. if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n";
  1833. $fieldPairs = static::collectFieldPairs('release', $langData, $release);
  1834. static::appendFieldList($content, $langData, $fieldPairs);
  1835. static::appendDetailSection($content, $langData, 'desc', $release->desc ?? null);
  1836. $markdown['content'] = implode("\n", $content);
  1837. $markdown['attrs'] = array(
  1838. 'objectTitle' => $markdown['title'],
  1839. 'system' => $release->system ?? '',
  1840. 'project' => $release->project ?? '',
  1841. 'build' => $release->build ?? '',
  1842. 'status' => $release->status ?? '',
  1843. );
  1844. return $markdown;
  1845. }
  1846. /**
  1847. * 将 TICKET 对象转换为 Markdown。
  1848. * Convert ticket object to Markdown format.
  1849. *
  1850. * @access public
  1851. * @param object $ticket
  1852. * @param array $langData
  1853. * @return array
  1854. */
  1855. public static function convertTicketToMarkdown($ticket, $langData = [])
  1856. {
  1857. $ticket = static::convertUserFieldToRealname('ticket', $ticket);
  1858. $id = $ticket->id ?? 0;
  1859. $title = trim((string)($ticket->title ?? $ticket->name ?? ''));
  1860. $typeName = isset($langData['common']) ? $langData['common'] : '';
  1861. $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title");
  1862. if($header === '') $header = "#$id $title";
  1863. $markdown = array('id' => $id, 'title' => $header);
  1864. $content = array();
  1865. $content[] = "# {$header}\n";
  1866. $sectionBasic = static::getSectionLabel($langData, 'basic');
  1867. if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n";
  1868. $fieldPairs = static::collectFieldPairs('ticket', $langData, $ticket);
  1869. static::appendFieldList($content, $langData, $fieldPairs);
  1870. static::appendDetailSection($content, $langData, 'desc', $ticket->desc ?? null);
  1871. static::appendDetailSection($content, $langData, 'resolution', $ticket->resolution ?? null);
  1872. static::appendDetailSection($content, $langData, 'history', $ticket->history ?? null);
  1873. $ticketType = $ticket->ticketType ?? $ticket->type ?? '';
  1874. $markdown['content'] = implode("\n", $content);
  1875. $markdown['attrs'] = array(
  1876. 'objectTitle' => $markdown['title'],
  1877. 'status' => $ticket->status ?? '',
  1878. 'type' => $ticketType,
  1879. 'pri' => $ticket->pri ?? '',
  1880. 'assignedTo' => $ticket->assignedTo ?? '',
  1881. 'product' => $ticket->product ?? '',
  1882. 'project' => $ticket->project ?? '',
  1883. 'customer' => $ticket->customer ?? '',
  1884. );
  1885. return $markdown;
  1886. }
  1887. }