official-office-knowledge-smoke-test.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. const assert = require('assert/strict');
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const {
  6. createDocumentKnowledgeService,
  7. createTodoKnowledgeService,
  8. unwrapOfficialResponse,
  9. } = require('../mcp/src/dashboard/official-office-knowledge-service');
  10. function rpc(payload) {
  11. return {
  12. status: 'ok',
  13. data: {
  14. response: {
  15. jsonrpc: '2.0',
  16. id: 1,
  17. result: { isError: false, content: [{ type: 'text', text: JSON.stringify(payload) }] },
  18. },
  19. },
  20. };
  21. }
  22. async function main() {
  23. const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-office-knowledge-'));
  24. const readyStatus = async () => ({ installed: true, valid: true, authorized: true, ready: true, installedVersion: '0.1.9' });
  25. const available = async () => ({ available: true, reason: 'available', message: '官方能力可用' });
  26. let docPolls = 0;
  27. const docCalls = [];
  28. const docOfficialCall = async request => {
  29. docCalls.push(request);
  30. if (request.method === 'create_doc') return rpc({ errcode: 0, docid: 'doc-live-1', url: 'https://doc.weixin.qq.com/doc/demo' });
  31. if (request.method === 'edit_doc_content') return rpc({ errcode: 0 });
  32. if (request.method === 'get_doc_content') {
  33. docPolls += 1;
  34. if (docPolls % 2 === 1) return rpc({ errcode: 0, task_id: 'task-1', task_done: false });
  35. return rpc({ errcode: 0, task_id: 'task-1', task_done: true, docid: 'doc-live-1', url: 'https://doc.weixin.qq.com/doc/demo', doc_name: '客户演示方案', content: '# 客户演示方案\n\n王刚负责在 7 月 20 日前整理演示清单。' });
  36. }
  37. throw new Error(`unexpected doc method ${request.method}`);
  38. };
  39. const analyzer = async () => ({
  40. status: 'completed', summary: '文档明确了客户演示准备任务。', keyPoints: ['准备客户演示'], decisions: [],
  41. actionItems: [{ title: '整理演示清单', owner: '王刚', dueDate: '2026-07-20', evidence: '文档原文明确说明', confidence: 0.98 }],
  42. risks: [], knowledgeTags: ['客户演示'], analysisBasis: '仅基于企微文档原文。', requiresReview: true, analyzedAt: new Date().toISOString(), error: null,
  43. });
  44. try {
  45. const docs = createDocumentKnowledgeService({ storeDir: path.join(tempRoot, 'docs'), officialStatus: readyStatus, officialCall: docOfficialCall, capability: available, analyzer, wait: async () => {} });
  46. const created = await docs.createDocument({ title: '客户演示方案', content: '# 客户演示方案\n\n正文' });
  47. assert.equal(created.status, 'ok');
  48. assert.equal(created.summary.live, true);
  49. assert.equal(created.data.document.sourceKind, 'official-cli-live');
  50. assert.match(created.data.document.markdown, /王刚负责/);
  51. assert.ok(fs.existsSync(created.data.document.files.json));
  52. assert.ok(fs.existsSync(created.data.document.files.markdown));
  53. assert.deepEqual(docCalls.slice(0, 2).map(item => item.method), ['create_doc', 'edit_doc_content']);
  54. assert.ok(docCalls.filter(item => item.method === 'get_doc_content').length >= 2);
  55. const analyzed = await docs.analyze('doc-live-1');
  56. assert.equal(analyzed.status, 'ok');
  57. assert.equal(analyzed.data.document.analysis.actionItems[0].owner, '王刚');
  58. assert.match(fs.readFileSync(analyzed.data.document.files.markdown, 'utf8'), /整理演示清单/);
  59. const docHub = await docs.hub();
  60. assert.equal(docHub.summary.docReady, true);
  61. assert.equal(docHub.summary.documentCount, 1);
  62. const permissionDocs = createDocumentKnowledgeService({
  63. storeDir: path.join(tempRoot, 'permission-docs'), officialStatus: readyStatus, capability: available, wait: async () => {},
  64. officialCall: async request => {
  65. if (request.method === 'create_doc') return rpc({ errcode: 0, docid: 'doc-permission-1', url: 'https://doc.weixin.qq.com/doc/permission' });
  66. if (request.method === 'edit_doc_content') return rpc({ errcode: 0 });
  67. return rpc({ errcode: 851008, errmsg: 'partial no authorization' });
  68. },
  69. });
  70. const permissionCreated = await permissionDocs.createDocument({ title: '待读权限文档', content: '# 已成功写入' });
  71. assert.equal(permissionCreated.status, 'ok');
  72. assert.equal(permissionCreated.summary.readbackPending, true);
  73. assert.equal(permissionCreated.data.document.readbackStatus, 'pending-permission');
  74. assert.equal(permissionCreated.data.document.sourceKind, 'official-cli-live-created');
  75. const todoCalls = [];
  76. let todoListEmpty = false;
  77. const todoOfficialCall = async request => {
  78. todoCalls.push(request);
  79. if (request.method === 'search_todo_userid') return rpc({ errcode: 0, user_list: [{ userid: 'wanggang', name: '王刚', alias: '刚sir' }] });
  80. if (request.method === 'get_todo_list') return rpc({ errcode: 0, todo_list: todoListEmpty ? [] : [
  81. { todo_id: 'todo-live-1', content: '整理客户资料', todo_status: 1, end_time: '2026-07-20 18:00:00', follower_list: { followers: [{ follower_id: 'wanggang' }] } },
  82. { todo_id: 'todo-live-2', content: '确认演示时间', todo_status: 0, follower_list: { followers: [{ follower_id: 'wanggang' }] } },
  83. ] });
  84. if (request.method === 'get_todo_detail') return rpc({ errcode: 0, data_list: [
  85. { todo_id: 'todo-live-3', content: '制作客户演示截图', todo_status: 1, end_time: '2026-07-21 18:00:00', follower_list: { followers: [{ follower_id: 'wanggang' }] } },
  86. ] });
  87. if (request.method === 'create_todo') return rpc({ errcode: 0, todo_id: 'todo-live-3' });
  88. if (request.method === 'update_todo') return rpc({ errcode: 0 });
  89. throw new Error(`unexpected todo method ${request.method}`);
  90. };
  91. const todos = createTodoKnowledgeService({ storeDir: path.join(tempRoot, 'todos'), officialStatus: readyStatus, officialCall: todoOfficialCall, capability: available });
  92. const users = await todos.searchUsers({ keyword: '王刚' });
  93. assert.equal(users.data.users[0].id, 'wanggang');
  94. const synced = await todos.sync({ followerId: 'wanggang' });
  95. assert.equal(synced.status, 'ok');
  96. assert.equal(synced.summary.syncedCount, 2);
  97. const todoCreated = await todos.create({ content: '制作客户演示截图', followerIds: ['wanggang'], endTime: '2026-07-21T18:00', remindType: 3 });
  98. assert.equal(todoCreated.status, 'ok');
  99. assert.equal(todoCreated.data.todo.id, 'todo-live-3');
  100. const refreshed = await todos.refreshDetails({ todoIds: ['todo-live-3'] });
  101. assert.equal(refreshed.summary.refreshedCount, 1);
  102. todoListEmpty = true;
  103. const preserved = await todos.sync({ followerId: 'wanggang' });
  104. assert.equal(preserved.summary.preservedCount, 3);
  105. const completed = await todos.complete('todo-live-3');
  106. assert.equal(completed.status, 'ok');
  107. assert.equal(todos.readIndex().todos.find(item => item.id === 'todo-live-3').status, 0);
  108. assert.ok(fs.existsSync(path.join(tempRoot, 'todos', 'todos.md')));
  109. const unwrapped = unwrapOfficialResponse(rpc({ errcode: 0, value: 'real' }));
  110. assert.equal(unwrapped.value, 'real');
  111. let blockedCalls = 0;
  112. const blockedDocs = createDocumentKnowledgeService({
  113. storeDir: path.join(tempRoot, 'blocked'), officialStatus: readyStatus,
  114. officialCall: async () => { blockedCalls += 1; throw new Error('blocked capability must not call live API'); },
  115. capability: async () => ({ available: false, reason: 'enterprise-policy', message: '文档能力未开放' }),
  116. });
  117. const blocked = await blockedDocs.importDocument({ docid: 'x' });
  118. assert.equal(blocked.status, 'error');
  119. assert.equal(blockedCalls, 0);
  120. process.stdout.write(`${JSON.stringify({ status: 'ok', officialEnvelopeUnwrapped: true, documentCreateReadAnalyze: true, documentReadPermissionFallback: true, todoSearchSyncCreateComplete: true, blockedPathMakesNoLiveCall: true }, null, 2)}\n`);
  121. } finally {
  122. fs.rmSync(tempRoot, { recursive: true, force: true });
  123. }
  124. }
  125. main().catch(error => {
  126. process.stderr.write(`${error.stack || error.message}\n`);
  127. process.exitCode = 1;
  128. });