dms-step0-verify.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * Step-0:切换前端到 DMS 之前的**小样本实测**。
  3. *
  4. * 为什么需要它:DMS 的 content 读写接口在文档里全是「📄 未实测」
  5. * (参数来自 SKILL.md/Apifox,OpenAPI 里是 @RequestBody 抓不到),
  6. * 而前端的 upsert 依赖三件没被证实的事:
  7. * ① addContent 是否要求 c_id(模型里 must=true)→ 若强校验,整个方案要改
  8. * ② addContent 返回体里有没有记录 uuid(有则省掉一次反查)
  9. * ③ timestamp 写入格式、states 默认值、长文本往返
  10. *
  11. * 怎么跑(在项目根目录):
  12. * node harness/tools/dms-step0-verify.mjs # token 自动取(见下)
  13. * node harness/tools/dms-step0-verify.mjs <DMS_TOKEN> # 也可以显式传
  14. *
  15. * token 的取法见 `dms-token.mjs`:参数 → 环境变量 → 本机文件 `.dms-token`
  16. * (由 `npm run dms:token` 写入,可从剪贴板读)。**不必每次手填**。
  17. *
  18. * 脚本会自行清理它写入的测试数据(session_id/record_id 以 step0_ 开头)。
  19. */
  20. import { readDmsToken, DMS_TOKEN_FILE } from './dms-token.mjs';
  21. const HOST = process.env.DMS_HOST || '121.43.55.7:10081';
  22. const BASE = `http://${HOST}/dms`;
  23. const TOKEN = readDmsToken(process.argv[2]);
  24. const COL_SESSION = 1887;
  25. const MODEL_SESSION = 2034;
  26. const COL_RECORD = 1889;
  27. const MODEL_RECORD = 2038;
  28. const SUFFIX = Date.now().toString(36);
  29. const SESSION_ID = `step0_sess_${SUFFIX}`;
  30. const RECORD_ID = `step0_rec_${SUFFIX}`;
  31. const CREDIT = 'step0_credit_code';
  32. if (!TOKEN) {
  33. console.error('缺 token。用法:node harness/tools/dms-step0-verify.mjs <DMS_TOKEN>');
  34. process.exit(2);
  35. }
  36. const results = [];
  37. const note = (name, ok, detail) => {
  38. results.push({ name, ok, detail });
  39. console.log(` ${ok === null ? '? ' : ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' → ' + detail : ''}`);
  40. };
  41. async function call(method, path, form) {
  42. const init = { method, headers: { token: TOKEN } };
  43. if (form) {
  44. init.headers['Content-Type'] = 'application/x-www-form-urlencoded';
  45. init.body = new URLSearchParams(form).toString();
  46. }
  47. const res = await fetch(`${BASE}${path}`, init);
  48. const text = await res.text();
  49. let json = null;
  50. try {
  51. json = JSON.parse(text);
  52. } catch {
  53. /* 非 JSON 原样返回 */
  54. }
  55. return { status: res.status, json, text };
  56. }
  57. const search = (columnId, field, value, extra = {}) =>
  58. call('POST', '/content/selectContentList', {
  59. columnId: String(columnId),
  60. page: '0',
  61. pageSize: '10',
  62. search: JSON.stringify([{ field, searchType: 1, content: { value } }]),
  63. ...extra,
  64. });
  65. const cleanup = async (columnId, field, value) => {
  66. const r = await search(columnId, field, value);
  67. const rows = r.json?.content?.data || [];
  68. for (const row of rows) {
  69. if (row?.id) await call('DELETE', `/content/delContentById?id=${encodeURIComponent(row.id)}`);
  70. }
  71. return rows.length;
  72. };
  73. const run = async () => {
  74. console.log(`\nDMS Step-0 实测 ${BASE} 标记=${SUFFIX}\n`);
  75. // ── 1. addContent:不传 c_id 会不会被拒(最高优先级)────────────────────
  76. console.log('【1】addContent(1887 会话)不传 c_id');
  77. const addSession = await call('POST', '/content/addContent', {
  78. columnId: String(COL_SESSION),
  79. modelId: String(MODEL_SESSION),
  80. content: JSON.stringify({
  81. c_credit_code: CREDIT,
  82. c_session_id: SESSION_ID,
  83. c_title: 'step0 会话标题',
  84. c_source: 'zhaoshang',
  85. }),
  86. });
  87. console.log(' 原始响应:', addSession.text.slice(0, 400));
  88. const addOk = addSession.json?.code === 200;
  89. note('addContent 不传 c_id 被接受', addOk, addOk ? '' : `code=${addSession.json?.code}`);
  90. const returnedUuid = addSession.json?.content?.id || addSession.json?.content?.uuid || null;
  91. note('addContent 响应回传记录 uuid', !!returnedUuid, returnedUuid ? String(returnedUuid) : '(未回传,需反查)');
  92. // ── 2. 反查:states 默认值 / c_id 是否自动生成 ──────────────────────────
  93. console.log('\n【2】反查(不带 states)');
  94. const found = await search(COL_SESSION, 'c_session_id', SESSION_ID);
  95. const rows = found.json?.content?.data || [];
  96. note('写后能查回(不传 states)', rows.length > 0, `命中 ${rows.length} 行, code=${found.json?.code}`);
  97. const row = rows[0] || {};
  98. console.log(' 行内容:', JSON.stringify(row).slice(0, 500));
  99. note('c_id 由 DMS 自动生成', row.c_id !== undefined && row.c_id !== null && row.c_id !== '', `c_id=${row.c_id}`);
  100. note('记录 uuid 存在行里(反查可取)', !!row.id, String(row.id || ''));
  101. const uuid = row.id || returnedUuid;
  102. // ── 3. timestamp 形态 ──────────────────────────────────────────────────
  103. console.log('\n【3】timestamp');
  104. note('c_created_at 未传时是否自动填充', row.c_created_at !== undefined && row.c_created_at !== null, `读到 ${JSON.stringify(row.c_created_at)}`);
  105. const tsFormats = ['2026-09-17 21:30:00', '2026-09-17T21:30:00+08:00'];
  106. for (const ts of tsFormats) {
  107. const r = await call('POST', '/content/updateContent', {
  108. columnId: String(COL_SESSION),
  109. modelId: String(MODEL_SESSION),
  110. content: JSON.stringify({ id: uuid, c_updated_at: ts }),
  111. });
  112. const back = await search(COL_SESSION, 'c_session_id', SESSION_ID);
  113. const got = back.json?.content?.data?.[0]?.c_updated_at;
  114. note(`写入 "${ts}"`, r.json?.code === 200 && got != null, `code=${r.json?.code}, 读回=${JSON.stringify(got)}`);
  115. }
  116. // ── 4. updateContent 改标题 ───────────────────────────────────────────
  117. console.log('\n【4】updateContent(带 id=uuid)');
  118. const upd = await call('POST', '/content/updateContent', {
  119. columnId: String(COL_SESSION),
  120. modelId: String(MODEL_SESSION),
  121. content: JSON.stringify({ id: uuid, c_title: 'step0 改后的标题' }),
  122. });
  123. const afterUpd = await search(COL_SESSION, 'c_session_id', SESSION_ID);
  124. const newTitle = afterUpd.json?.content?.data?.[0]?.c_title;
  125. note('改标题生效', upd.json?.code === 200 && newTitle === 'step0 改后的标题', `code=${upd.json?.code}, c_title=${JSON.stringify(newTitle)}`);
  126. // ── 5. 长文本 + 中文 + 协议标记往返 ────────────────────────────────────
  127. console.log('\n【5】1889 长文本往返(含协议标记)');
  128. const longAnswer = [
  129. '<!-- POLICY_TABLE {"data":[{"title":"测试政策"}]} -->',
  130. '<scope title="正在思考中...">\n</scope>',
  131. '第一段中文回答。'.repeat(120),
  132. '<question-cards>{"kind":"company_need"}</question-cards>',
  133. '结尾行',
  134. ].join('\n');
  135. const addRecord = await call('POST', '/content/addContent', {
  136. columnId: String(COL_RECORD),
  137. modelId: String(MODEL_RECORD),
  138. content: JSON.stringify({
  139. c_credit_code: CREDIT,
  140. c_session_id: SESSION_ID,
  141. c_record_id: RECORD_ID,
  142. c_question: 'step0 问题:高新技术企业奖励是多少?',
  143. c_answer: longAnswer,
  144. c_source: 'zhaoshang',
  145. }),
  146. });
  147. note('1889 addContent 成功', addRecord.json?.code === 200, `code=${addRecord.json?.code}`);
  148. const recRows = (await search(COL_RECORD, 'c_record_id', RECORD_ID)).json?.content?.data || [];
  149. const rec = recRows[0] || {};
  150. note('长文本逐字节一致', rec.c_answer === longAnswer, `原长 ${longAnswer.length} / 读回 ${String(rec.c_answer || '').length}`);
  151. note('中文问题往返一致', rec.c_question === 'step0 问题:高新技术企业奖励是多少?', JSON.stringify(rec.c_question));
  152. // ── 6. 反馈字段 update ────────────────────────────────────────────────
  153. console.log('\n【6】1889 反馈字段');
  154. const fb = await call('POST', '/content/updateContent', {
  155. columnId: String(COL_RECORD),
  156. modelId: String(MODEL_RECORD),
  157. content: JSON.stringify({
  158. id: rec.id,
  159. c_feedback_status: 2,
  160. c_feedback_option: '没有回答我的问题',
  161. c_feedback_remark: 'step0 备注',
  162. c_feedback_at: '2026-09-17 21:31:00',
  163. }),
  164. });
  165. const recAfter = (await search(COL_RECORD, 'c_record_id', RECORD_ID)).json?.content?.data?.[0] || {};
  166. note(
  167. '反馈字段写入生效',
  168. fb.json?.code === 200 && Number(recAfter.c_feedback_status) === 2 && recAfter.c_feedback_option === '没有回答我的问题',
  169. `code=${fb.json?.code}, status=${recAfter.c_feedback_status}, option=${JSON.stringify(recAfter.c_feedback_option)}, at=${JSON.stringify(recAfter.c_feedback_at)}`
  170. );
  171. // ── 7. 中文 search 精确匹配(访客 访客_xxx 依赖)──────────────────────
  172. console.log('\n【7】中文精确 search');
  173. const cn = await search(COL_SESSION, 'c_credit_code', CREDIT);
  174. note('中文/下划线 credit_code 精确命中', (cn.json?.content?.data || []).length > 0, `命中 ${(cn.json?.content?.data || []).length} 行`);
  175. // ── 8. pageSize 上限 ─────────────────────────────────────────────────
  176. console.log('\n【8】pageSize');
  177. const big = await call('POST', '/content/selectContentList', {
  178. columnId: String(COL_RECORD),
  179. page: '0',
  180. pageSize: '200',
  181. search: JSON.stringify([{ field: 'c_session_id', searchType: 1, content: { value: SESSION_ID } }]),
  182. });
  183. note('pageSize=200 被接受', big.json?.code === 200 || big.json?.code === 202, `code=${big.json?.code}, 返回 ${(big.json?.content?.data || []).length} 行`);
  184. // ── 9. delContentById 参数形态 ───────────────────────────────────────
  185. console.log('\n【9】delContentById 参数形态');
  186. const probe = await call('DELETE', `/content/delContentById?id=${encodeURIComponent(rec.id)}`);
  187. const stillThere = ((await search(COL_RECORD, 'c_record_id', RECORD_ID)).json?.content?.data || []).length;
  188. note('DELETE ?id=<uuid> 生效', stillThere === 0, `code=${probe.json?.code}, 剩余 ${stillThere} 行`);
  189. // ── 10. 清理 1887 测试行 ─────────────────────────────────────────────
  190. console.log('\n【10】清理');
  191. const removed = await cleanup(COL_SESSION, 'c_session_id', SESSION_ID);
  192. const left = await cleanup(COL_RECORD, 'c_record_id', RECORD_ID);
  193. note('测试数据已清理', true, `1887 删 ${removed} 行 / 1889 删 ${left} 行`);
  194. // ── 汇总 ─────────────────────────────────────────────────────────────
  195. const failed = results.filter((r) => r.ok === false);
  196. console.log(`\n===== 汇总:${results.length - failed.length}/${results.length} 通过 =====`);
  197. if (failed.length) {
  198. console.log('未通过项:');
  199. failed.forEach((f) => console.log(' - ' + f.name + (f.detail ? ' ' + f.detail : '')));
  200. }
  201. console.log('\n关键结论(回填到计划/记录里):');
  202. console.log(' addContent 是否需 c_id :', addOk ? '不需要(自动生成)' : '被拒 → 需 DMS 侧调整模型');
  203. console.log(' addContent 是否回 uuid :', returnedUuid ? '回传,可省一次反查' : '不回传,需 search 反查');
  204. console.log(' 时间戳 : 见【3】读回值');
  205. };
  206. run().catch((err) => {
  207. console.error('\n脚本异常:', err);
  208. process.exit(1);
  209. });