dsh-contextos-devops 0.0.0-stage → 1.48.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -2
  3. package/assets/avatar.jpg +0 -0
  4. package/assets/client.css +3316 -0
  5. package/assets/icon-180.png +0 -0
  6. package/build/client.mjs +64 -0
  7. package/client.js +7622 -0
  8. package/cordis.patch.yml +3 -0
  9. package/icon.svg +15 -0
  10. package/index.js +345 -0
  11. package/lib/base/env.js +22 -0
  12. package/lib/base/field.js +25 -0
  13. package/lib/base/http.js +140 -0
  14. package/lib/base/platform.js +33 -0
  15. package/lib/base/registry.js +186 -0
  16. package/lib/base/state.js +66 -0
  17. package/lib/base/table.js +47 -0
  18. package/lib/base/texttable.js +26 -0
  19. package/lib/base/url.js +178 -0
  20. package/lib/core/README.md +17 -0
  21. package/lib/core/apps.js +675 -0
  22. package/lib/core/auth.js +110 -0
  23. package/lib/core/avatar.js +289 -0
  24. package/lib/core/backup.js +105 -0
  25. package/lib/core/codegen.js +401 -0
  26. package/lib/core/copilot.js +419 -0
  27. package/lib/core/data.js +157 -0
  28. package/lib/core/device.js +148 -0
  29. package/lib/core/files.js +152 -0
  30. package/lib/core/gobuild.js +63 -0
  31. package/lib/core/iam.js +122 -0
  32. package/lib/core/identity.js +22 -0
  33. package/lib/core/nodes.js +627 -0
  34. package/lib/core/ops.js +306 -0
  35. package/lib/core/project.js +393 -0
  36. package/lib/core/spaces.js +406 -0
  37. package/lib/core/terminal.js +137 -0
  38. package/lib/core/terminal2.js +141 -0
  39. package/lib/core/users.js +51 -0
  40. package/lib/core/vocab.js +158 -0
  41. package/lib/misc/async.js +15 -0
  42. package/lib/misc/shell.js +27 -0
  43. package/lib/misc/static.js +33 -0
  44. package/locale/en.json +10 -0
  45. package/locale/zh.json +10 -0
  46. package/package.json +92 -4
  47. package/src/client/README.md +37 -0
  48. package/src/client/parts/00-prelude.js +315 -0
  49. package/src/client/parts/05-bootstrap.js +61 -0
  50. package/src/client/parts/08-store.js +85 -0
  51. package/src/client/parts/10-ui.js +188 -0
  52. package/src/client/parts/12-debug.js +361 -0
  53. package/src/client/parts/15-layout.js +86 -0
  54. package/src/client/parts/20-devworkspace.js +789 -0
  55. package/src/client/parts/25-editor-ui.js +28 -0
  56. package/src/client/parts/30-modals.js +85 -0
  57. package/src/client/parts/32-cards.js +176 -0
  58. package/src/client/parts/34-forms.js +176 -0
  59. package/src/client/parts/36-data.js +267 -0
  60. package/src/client/parts/40-project.js +430 -0
  61. package/src/client/parts/45-map.js +109 -0
  62. package/src/client/parts/46-pm.js +158 -0
  63. package/src/client/parts/47-three.js +324 -0
  64. package/src/client/parts/49-msg.js +167 -0
  65. package/src/client/parts/50-chat.js +1422 -0
  66. package/src/client/parts/51-pmselection.js +31 -0
  67. package/src/client/parts/52-pmdata.js +29 -0
  68. package/src/client/parts/55-pm.js +146 -0
  69. package/src/client/parts/58-config.js +381 -0
  70. package/src/client/parts/59-monitor.js +262 -0
  71. package/src/client/parts/60-page.js +1518 -0
  72. package/src/client/styles/kit.css +20 -0
  73. package/src/client/styles/main.css +3294 -0
  74. package/src/client/styles/tokens.css +2 -0
  75. package/vendor/leaflet/leaflet.css +661 -0
  76. package/vendor/leaflet/leaflet.js +6 -0
  77. package/vendor/three/three.module.js +53044 -0
  78. package/vendor/xterm/addon-fit.js +2 -0
  79. package/vendor/xterm/xterm.css +218 -0
  80. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,393 @@
1
+ // 核心业务:项目管理(target/plan/task/report/feedback,SQLite 直读 + 翻译缓存)。
2
+ import path from 'node:path';
3
+ import net from 'node:net';
4
+ import { http, encode, envelopeRows } from '../base/http.js';
5
+ import { listNodes } from '../base/registry.js';
6
+ import { runShell } from '../misc/shell.js';
7
+ import { listAvatars } from './avatar.js';
8
+ import { pmCreate, pmAction } from './copilot.js';
9
+
10
+ const PLAN_STATUS = ['created', 'submited', 'approved', 'process', 'finished', 'canceled', 'rejected'];const PM_STATUS_ORDER = ['created', 'submited', 'approved', 'process', 'finished', 'canceled', 'rejected'];
11
+ function sortPmItems(items) {
12
+ const rank = new Map(PM_STATUS_ORDER.map((name, i) => [name, i]));
13
+ return (items || []).slice().sort((a, b) => {
14
+ const ra = rank.has(a.status_name) ? rank.get(a.status_name) : 99;
15
+ const rb = rank.has(b.status_name) ? rank.get(b.status_name) : 99;
16
+ if (ra !== rb) return ra - rb;
17
+ return String(b.created_at || b.updated_at || '').localeCompare(String(a.created_at || a.updated_at || ''));
18
+ });
19
+ }
20
+
21
+ const TASK_STATUS = ['created', 'submited', 'approved', 'process', 'finished', 'canceled', 'rejected'];
22
+ const FB_STATUS = ['canFinish', 'shouldImprove'];
23
+ const REPORT_TYPE = ['issue', 'daily', 'weekly', 'monthly', 'quarterly', 'annual'];
24
+ const PM_KIND_TEXT = { target: ['目标', 'TARGET'], plan: ['计划', 'PLAN'], task: ['任务', 'TASK'], report: ['报告', 'REPORT'], feedback: ['反馈', 'FEEDBACK'] };
25
+ const STATUS_ZH = { created: '待提交', submited: '待审批', approved: '待开始', process: '进行中', finished: '已完成', canceled: '已取消', rejected: '已驳回' };
26
+ const FEEDBACK_ZH = { canFinish: '可完成', shouldImprove: '待改进' };
27
+ const REPORT_TYPE_ZH = { issue: '问题', daily: '日报', weekly: '周报', monthly: '月报', quarterly: '季报', annual: '年报' };
28
+ const FIELD_ZH = { title: '标题', content: '内容', begin_time: '开始时间', end_time: '结束时间', report_type: '报告类型', feedback_status: '反馈状态', score: '评分' };
29
+ const UID_RE = /^[0-9a-f]{32}$/i;
30
+ const UID_ALIASES = {
31
+ targetUID: 'target_uid', targetId: 'target_uid', target_uid: 'target_uid',
32
+ planUID: 'plan_uid', planId: 'plan_uid', plan_uid: 'plan_uid',
33
+ taskUID: 'task_uid', taskId: 'task_uid', task_uid: 'task_uid',
34
+ reportUID: 'report_uid', reportId: 'report_uid', report_uid: 'report_uid',
35
+ feedbackUID: 'feedback_uid', feedbackId: 'feedback_uid', feedback_uid: 'feedback_uid',
36
+ };
37
+ function normalizeUidOptions(input = {}) {
38
+ const out = {};
39
+ for (const [key, value] of Object.entries(input || {})) {
40
+ const canonical = UID_ALIASES[key];
41
+ if (!canonical || !value) continue;
42
+ if (!UID_RE.test(String(value))) throw new Error(`invalid ${canonical}: expected 32-char hash`);
43
+ out[canonical] = String(value);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ function pmActionNames(kind, item, ctx = {}) {
49
+ const status = item.status_name || (STATUS_ZH[item.status] ? item.status : PLAN_STATUS[Number(item.status)]) || String(item.status || '');
50
+ if (kind === 'target') return ['planCreate', 'modify', ...((ctx.plans || []).length ? [] : ['remove'])];
51
+ if (kind === 'plan') {
52
+ if (status === 'created') return ['submit', 'modify', 'remove'];
53
+ if (status === 'submited') return ['approve', 'reject'];
54
+ if (status === 'approved') return ['process', 'modify', 'remove'];
55
+ if (status === 'process') {
56
+ const actions = ['taskCreate'];
57
+ if (!(ctx.tasks || []).some((t) => t.status_name === 'process')) actions.push('finish', 'cancel');
58
+ return actions;
59
+ }
60
+ return [];
61
+ }
62
+ if (kind === 'task') {
63
+ if (status === 'created') return ['process', 'handover', 'modify', 'remove'];
64
+ if (status === 'process') {
65
+ const actions = ['reportCreate', 'handover', 'cancel'];
66
+ if (item.can_finish) actions.push('finish');
67
+ return actions;
68
+ }
69
+ return [];
70
+ }
71
+ if (kind === 'report') {
72
+ const taskStatus = (ctx.task || {}).status_name || '';
73
+ const taskProcess = taskStatus === 'process';
74
+ // 任务完成/取消后,报告不能再创建反馈
75
+ const actions = taskProcess ? ['feedbackCreate'] : [];
76
+ if (item.link) actions.push('open');
77
+ if (taskProcess && !(ctx.feedbacks || []).length) actions.push('modify', 'remove');
78
+ return actions;
79
+ }
80
+ if (kind === 'feedback') return (ctx.task || {}).status_name === 'process' ? ['modify', 'remove'] : [];
81
+ return [];
82
+ }
83
+
84
+ export async function fieldOptions({ node, space = '', command = '', field = '', projectUid = '', uid = '', sessid = '' } = {}) {
85
+ if (!space || !command || !field) throw new Error('space/command/field are required');
86
+ const q = new URLSearchParams({ 'user.uid': uid });
87
+ if (projectUid) q.set('project_uid', projectUid);
88
+ const r = await http(node, `/s/${encode(space)}/c/web.team.xiangmuguanli.portal/action/run/${command}/action/inputs/${encode(field)}?${q}`, { timeout: 12000 });
89
+ let env = {};
90
+ try { env = JSON.parse(r.text); } catch { throw new Error(`inputs ${field} returned non-JSON`); }
91
+ const values = Array.isArray(env[field]) ? env[field].map(String) : [];
92
+ const names = Array.isArray(env.name) ? env.name.map(String) : [];
93
+ const fieldLabel = FIELD_ZH[field] || field;
94
+ const valueTrans = field === 'report_type' ? REPORT_TYPE_ZH : field === 'feedback_status' ? FEEDBACK_ZH : {};
95
+ const options = values.map((value, i) => {
96
+ const name = names[i] || value;
97
+ return { value, name, label: valueTrans[name] || valueTrans[value] || name };
98
+ });
99
+ return { field, field_label: fieldLabel, values, names, options };
100
+ }
101
+
102
+ function grabTransMap(text, key) {
103
+ const source = String(text || '').replace(/\\"/g, '"');
104
+ const marker = `"${key}":{`;
105
+ const start = source.indexOf(marker);
106
+ if (start < 0) return {};
107
+ let depth = 0, end = -1;
108
+ for (let i = start + marker.length - 1; i < source.length; i++) {
109
+ if (source[i] === '{') depth++;
110
+ else if (source[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
111
+ }
112
+ if (end < 0) return {};
113
+ try { return JSON.parse(source.slice(start + marker.length - 1, end + 1)); } catch { return {}; }
114
+ }
115
+ async function pmApiRows({ node, space, projectUid, command, uid, sessid = '', extra = '' }) {
116
+ const q = new URLSearchParams({ project_uid: projectUid, 'user.uid': uid });
117
+ if (sessid) q.set('sessid', sessid);
118
+ const url = `/s/${encode(space)}/c/web.team.xiangmuguanli.portal/action/run/web.team.xiangmuguanli.${command}/${encode(projectUid)}?${q}${extra ? `&${extra}` : ''}`;
119
+ const r = await http(node, url, { timeout: 20000 });
120
+ return envelopeRows(r.text).rows;
121
+ }
122
+
123
+ async function pmActionRows({ node, space, projectUid, command, action, parentUid, uid }) {
124
+ const q = new URLSearchParams({ project_uid: projectUid, 'user.uid': uid });
125
+ if (parentUid) q.set('uid', parentUid);
126
+ const base = `/s/${encode(space)}/c/web.team.xiangmuguanli.portal/action/run/web.team.xiangmuguanli.${command}`;
127
+ const r = await http(node, `${base}/action/${action}?${q}`, { timeout: 20000 });
128
+ return envelopeRows(r.text).rows;
129
+ }
130
+
131
+ async function pmTranslations() {
132
+ return { status: STATUS_ZH, feedback_status: FEEDBACK_ZH, report_type: REPORT_TYPE_ZH };
133
+ }
134
+
135
+ export async function pmList({ node, avatarUid = '', lang = 'zh', targetUid = '', planUid = '', taskUid = '', reportUid = '', scope = 'target' } = {}) {
136
+ const avatars = await listAvatars({ node });
137
+ const avatar = (avatars.items || []).find((x) => x.uid === (avatarUid || avatars.active));
138
+ if (!avatar) throw new Error('avatar not found');
139
+ const project = avatar.project;
140
+ if (!project?.place_uid) return { avatar, project: null, targets: [], plans: [], tasks: [], reports: [], feedbacks: [] };
141
+ const projectUid = project.place_uid;
142
+ const sessid = '';
143
+ const trans = lang === 'zh' ? await pmTranslations({ node, space: project.space, projectUid, uid: avatar.uid }).catch(() => ({ status: {}, feedback_status: {}, report_type: {} })) : { status: {}, feedback_status: {}, report_type: {} };
144
+ const base = { node, space: project.space, projectUid, uid: avatar.uid, sessid };
145
+ const order = ['target', 'plan', 'task', 'report', 'feedback'];
146
+ const at = order.indexOf(scope) >= 0 ? order.indexOf(scope) : 0;
147
+ const targets = at === 0 ? (await pmApiRows({ ...base, command: 'target' })).filter((x) => String(x.title || '').trim()) : [];
148
+ const selectedTarget = targetUid || targets[0]?.uid || '';
149
+ const plans = at <= 1 && selectedTarget ? await pmActionRows({ ...base, command: 'target', action: 'planList', parentUid: selectedTarget }) : [];
150
+ const selectedPlan = planUid || plans[0]?.uid || '';
151
+ const tasks = at <= 2 && selectedPlan ? await pmActionRows({ ...base, command: 'plan', action: 'taskList', parentUid: selectedPlan }) : [];
152
+ const selectedTask = taskUid || tasks[0]?.uid || '';
153
+ const reports = at <= 3 && selectedTask ? await pmActionRows({ ...base, command: 'task', action: 'reportList', parentUid: selectedTask }) : [];
154
+ const selectedReport = reportUid || reports[0]?.uid || '';
155
+ const feedbacks = at <= 4 && selectedReport ? await pmActionRows({ ...base, command: 'report', action: 'feedbackList', parentUid: selectedReport }) : [];
156
+ for (const p of plans) p.status_name = PLAN_STATUS[Number(p.status)] || String(p.status);
157
+ for (const t of tasks) t.status_name = TASK_STATUS[Number(t.status)] || String(t.status);
158
+ for (const f of feedbacks) {
159
+ f.status = f.feedback_status;
160
+ const raw = String(f.feedback_status ?? '');
161
+ f.status_name = /^\d+$/.test(raw) ? (FB_STATUS[Number(raw)] || raw) : raw;
162
+ }
163
+ for (const r of reports) { r.type = r.report_type; r.type_name = REPORT_TYPE[Number(r.report_type)] || String(r.report_type || ''); }
164
+ for (const p of plans) p.status_label = lang === 'zh' ? (trans.status[p.status_name] || p.status_name) : p.status_name;
165
+ for (const k of tasks) k.status_label = lang === 'zh' ? (trans.status[k.status_name] || k.status_name) : k.status_name;
166
+ for (const f of feedbacks) f.status_label = lang === 'zh' ? (trans.feedback_status[f.status_name] || f.status_name) : f.status_name;
167
+ for (const r of reports) r.type_label = lang === 'zh' ? (trans.report_type[r.type_name] || r.type_name) : r.type_name;
168
+ for (const r of reports) r.status_label = r.type_label;
169
+ for (const [kind, list] of Object.entries({ target: targets, plan: plans, task: tasks, report: reports, feedback: feedbacks })) {
170
+ const text = PM_KIND_TEXT[kind] || [kind, kind.toUpperCase()];
171
+ for (const item of list) { item.kind = kind; item.kind_label = lang === 'zh' ? text[0] : text[1]; }
172
+ }
173
+ for (const r of reports) r.feedbacks = feedbacks.filter((f) => f.report_uid === r.uid);
174
+ for (const t of tasks) t.reports = reports.filter((r) => r.task_uid === t.uid);
175
+ for (const p of plans) p.tasks = tasks.filter((t) => t.plan_uid === p.uid);
176
+ for (const t of targets) t.plans = plans.filter((p) => p.target_uid === t.uid);
177
+ for (const t of tasks) t.can_finish = (t.reports || []).some((r) => (r.feedbacks || []).some((f) => f.status_name === 'canFinish'));
178
+ for (const t of targets) t.actions = pmActionNames('target', t, { plans: t.plans || [] });
179
+ for (const p of plans) p.actions = pmActionNames('plan', p, { tasks: p.tasks || [] });
180
+ for (const t of tasks) t.actions = pmActionNames('task', t, { reports: t.reports || [] });
181
+ for (const r of reports) r.actions = pmActionNames('report', r, { task: tasks.find((t) => t.uid === r.task_uid), feedbacks: r.feedbacks || [] });
182
+ for (const f of feedbacks) f.actions = pmActionNames('feedback', f, { task: tasks.find((t) => t.uid === (reports.find((r) => r.uid === f.report_uid) || {}).task_uid) });
183
+ const sortedPlans = sortPmItems(plans);
184
+ const sortedTasks = sortPmItems(tasks);
185
+ for (const p of plans) p.tasks = sortPmItems(p.tasks || []);
186
+ for (const t of targets) t.plans = sortPmItems(t.plans || []);
187
+ return { avatar, project, scope, targets, plans: sortedPlans, tasks: sortedTasks, reports, feedbacks };
188
+ }
189
+
190
+ // 表单取值规范:枚举字段一律提交后台返回的 value(数字/编码),不要提交 name/label
191
+ const OPTION_CACHE = new Map();
192
+ export async function optionValue({ node, space, command, field, raw, projectUid = '', uid = '', sessid = '' }) {
193
+ if (raw === '' || raw === undefined || raw === null) return raw;
194
+ const key = `${node}|${space}|${command}|${field}`;
195
+ let opts = OPTION_CACHE.get(key);
196
+ if (!opts) {
197
+ try { const d = await fieldOptions({ node, space, command, field, projectUid, uid, sessid }); opts = d.options || []; }
198
+ catch { opts = []; }
199
+ OPTION_CACHE.set(key, opts);
200
+ }
201
+ const hit = opts.find((o) => String(o.value) === String(raw) || String(o.name) === String(raw) || String(o.label) === String(raw));
202
+ return hit ? String(hit.value) : String(raw);
203
+ }
204
+
205
+ export async function pmCreateItem({ node, avatarUid = '', kind = '', title = '', content = '', targetUid = '', planUid = '', taskUid = '', reportUid = '', beginTime = '', endTime = '', reportType = 'daily', score = '5', feedbackStatus = 'canFinish', uidOptions = {} } = {}) {
206
+ const avatars = await listAvatars({ node });
207
+ const avatar = (avatars.items || []).find((x) => x.uid === (avatarUid || avatars.active));
208
+ if (!avatar?.project?.space) throw new Error('avatar has no project');
209
+ const project = avatar.project;
210
+ const base = { node, space: project.space, projectUid: project.place_uid, actorUid: avatar.uid, sessid: '' };
211
+ const uidFields = normalizeUidOptions(uidOptions);
212
+ const send = (command, action, uid, fields = {}, extra = {}) => pmAction({ ...base, command: `web.team.xiangmuguanli.${command}`, action, uid, fields, method: 'PUT', extra });
213
+ if (kind === 'target') {
214
+ const uid = await pmCreate({ ...base, uid: avatar.uid, command: 'web.team.xiangmuguanli.target', fields: { title: title || '新目标', content } });
215
+ if (!uid) throw new Error('target create returned no uid');
216
+ return { kind, uid };
217
+ }
218
+ if (kind === 'plan') {
219
+ if (!targetUid) throw new Error('plan needs a target');
220
+ await send('target', 'planCreate', targetUid, { title: title || '新计划', content, begin_time: beginTime || new Date().toISOString().slice(0, 10), end_time: endTime || new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10) }, uidFields);
221
+ const rows = await pmActionRows({ node, space: project.space, projectUid: project.place_uid, command: 'target', action: 'planList', parentUid: targetUid, uid: avatar.uid });
222
+ const created = (rows || []).find((x) => x.title === (title || '新计划')) || (rows || [])[0];
223
+ return { kind, uid: created?.uid || '' };
224
+ }
225
+ if (kind === 'task') {
226
+ if (!planUid) throw new Error('task needs a plan');
227
+ await send('plan', 'taskCreate', planUid, { title: title || '新任务', content }, uidFields);
228
+ const rows = await pmActionRows({ node, space: project.space, projectUid: project.place_uid, command: 'plan', action: 'taskList', parentUid: planUid, uid: avatar.uid });
229
+ const created = (rows || []).find((x) => x.title === (title || '新任务')) || (rows || [])[0];
230
+ return { kind, uid: created?.uid || '' };
231
+ }
232
+ if (kind === 'report') {
233
+ if (!taskUid) throw new Error('report needs a task');
234
+ const rt = await optionValue({ node, space: project.space, command: 'web.team.xiangmuguanli.report', field: 'report_type', raw: reportType, projectUid: project.place_uid, uid: avatar.uid });
235
+ await send('task', 'reportCreate', taskUid, { report_type: rt, title: title || '进度报告', content }, uidFields);
236
+ const rows = await pmActionRows({ node, space: project.space, projectUid: project.place_uid, command: 'task', action: 'reportList', parentUid: taskUid, uid: avatar.uid });
237
+ const created = (rows || []).find((x) => x.title === (title || '进度报告')) || (rows || [])[0];
238
+ return { kind, uid: created?.uid || '' };
239
+ }
240
+ if (kind === 'feedback') {
241
+ if (!reportUid) throw new Error('feedback needs a report');
242
+ // 任务已结束(完成/取消)时禁止再建反馈
243
+ try {
244
+ const tree = await pmList({ node, avatarUid });
245
+ const taskUidOfReport = ((tree.reports || []).find((r) => r.uid === reportUid) || {}).task_uid;
246
+ const taskItem = (tree.tasks || []).find((t) => t.uid === taskUidOfReport) || {};
247
+ if (['finished', 'canceled'].includes(taskItem.status_name)) throw new Error('任务已结束,不能再创建反馈');
248
+ } catch (e) { if (String(e.message).includes('任务已结束')) throw e; }
249
+ const fs = await optionValue({ node, space: project.space, command: 'web.team.xiangmuguanli.feedback', field: 'feedback_status', raw: feedbackStatus, projectUid: project.place_uid, uid: avatar.uid });
250
+ const sc = await optionValue({ node, space: project.space, command: 'web.team.xiangmuguanli.feedback', field: 'score', raw: score, projectUid: project.place_uid, uid: avatar.uid });
251
+ await send('report', 'feedbackCreate', reportUid, { title: title || '反馈', content, score: sc, feedback_status: fs }, uidFields);
252
+ const rows = await pmActionRows({ node, space: project.space, projectUid: project.place_uid, command: 'report', action: 'feedbackList', parentUid: reportUid, uid: avatar.uid });
253
+ const created = (rows || []).find((x) => x.title === (title || '反馈')) || (rows || [])[0];
254
+ return { kind, uid: created?.uid || '' };
255
+ }
256
+ throw new Error('kind must be target/plan/task/report/feedback');
257
+ }
258
+
259
+ export async function pmLog({ node, space = '', logId = '' } = {}) {
260
+ if (!/^[0-9a-zA-Z_-]+$/.test(String(logId || ''))) throw new Error('bad log id');
261
+ const n = (await listNodes()).find((x) => x.port === Number(node));
262
+ if (!n?.dir || !space) throw new Error('node/space required');
263
+ const file = path.join(n.dir, 'usr', 'local', 'work', space, 'var', 'log', 'bench.log');
264
+ const r = await runShell(`grep -F ${JSON.stringify(String(logId))} ${JSON.stringify(file)} | tail -n 200`, process.cwd(), { timeoutMs: 5000 }).catch(() => null);
265
+ return { file, logId, text: String(r?.out || '') || `未找到日志:${logId}` };
266
+ }
267
+
268
+
269
+ // 每类对象只允许提交自己的表单字段,防止 plan 修改带上 report/feedback 的字段
270
+ export const PM_FIELDS = {
271
+ target: ['title', 'content'],
272
+ plan: ['title', 'content', 'begin_time', 'end_time'],
273
+ task: ['title', 'content'],
274
+ report: ['title', 'content', 'report_type'],
275
+ feedback: ['title', 'content', 'score', 'feedback_status'],
276
+ };
277
+
278
+ export function pmFieldsFor(kind, src = {}) {
279
+ const keys = PM_FIELDS[kind] || ['title', 'content'];
280
+ const out = {};
281
+ for (const k of keys) {
282
+ const v = src[k];
283
+ if (v === undefined || v === null || String(v) === '') continue;
284
+ out[k] = v;
285
+ }
286
+ return out;
287
+ }
288
+
289
+ export async function pmModifyItem({ node, avatarUid = '', kind = '', uid = '', title = '', content = '', beginTime = '', endTime = '', reportType = '', score = '', feedbackStatus = '', uidOptions = {} } = {}) {
290
+ const tree = await pmList({ node, avatarUid });
291
+ if (!tree.project?.space) throw new Error('avatar has no project');
292
+ const commands = { target: 'target', plan: 'plan', task: 'task', report: 'report', feedback: 'feedback' };
293
+ const command = commands[kind];
294
+ if (!command || !uid) throw new Error('kind and uid are required');
295
+ const space = tree.project.space;
296
+ const resolve = (field, raw) => optionValue({ node, space, command: `web.team.xiangmuguanli.${command}`, field, raw, projectUid: tree.project.place_uid, uid: tree.avatar?.uid || '' });
297
+ const fields = pmFieldsFor(kind, {
298
+ title, content,
299
+ begin_time: beginTime, end_time: endTime,
300
+ report_type: reportType ? await resolve('report_type', reportType) : '',
301
+ score: score ? await resolve('score', score) : '',
302
+ feedback_status: feedbackStatus ? await resolve('feedback_status', feedbackStatus) : '',
303
+ });
304
+ if (!Object.keys(fields).length) throw new Error('no fields to update');
305
+ const sessid = '';
306
+ const item = (tree[kind + 's'] || []).find((x) => x.uid === uid) || {};
307
+ await pmAction({ node, space: tree.project.space, command: `web.team.xiangmuguanli.${command}`, action: 'modify', uid, fields, projectUid: tree.project.place_uid, actorUid: tree.avatar.uid, sessid, extra: { ...normalizeUidOptions(item), ...normalizeUidOptions(uidOptions) } });
308
+ return { kind, uid, fields };
309
+ }
310
+
311
+ export async function pmRemoveItem({ node, avatarUid = '', kind = '', uid = '', uidOptions = {} } = {}) {
312
+ const tree = await pmList({ node, avatarUid });
313
+ if (!tree.project?.space) throw new Error('avatar has no project');
314
+ const commands = { target: 'target', plan: 'plan', task: 'task', report: 'report', feedback: 'feedback' };
315
+ const command = commands[kind];
316
+ if (!command || !uid) throw new Error('kind and uid are required');
317
+ const sessid = '';
318
+ const item = (tree[kind + 's'] || []).find((x) => x.uid === uid) || {};
319
+ await pmAction({ node, space: tree.project.space, command: `web.team.xiangmuguanli.${command}`, action: 'remove', uid, projectUid: tree.project.place_uid, actorUid: tree.avatar.uid, sessid, method: 'DELETE', extra: { ...normalizeUidOptions(item), ...normalizeUidOptions(uidOptions) } });
320
+ return { kind, uid, deleted: true };
321
+ }
322
+
323
+ export async function pmActItem({ node, avatarUid = '', kind = '', uid = '', action = '', uidOptions = {} } = {}) {
324
+ const tree = await pmList({ node, avatarUid });
325
+ if (!tree.project?.space) throw new Error('avatar has no project');
326
+ const commands = { target: 'target', plan: 'plan', task: 'task', report: 'report', feedback: 'feedback' };
327
+ const command = commands[kind];
328
+ if (!command || !uid || !action) throw new Error('kind/uid/action are required');
329
+ const sessid = '';
330
+ const item = tree[kind + 's']?.find((x) => x.uid === uid) || {};
331
+ await pmAction({ node, space: tree.project.space, command: `web.team.xiangmuguanli.${command}`, action, uid, projectUid: tree.project.place_uid, actorUid: tree.avatar.uid, sessid, extra: { ...normalizeUidOptions(item), ...normalizeUidOptions(uidOptions) } });
332
+ return { kind, uid, action };
333
+ }
334
+
335
+ export async function freePort({ node = 0, start = 9188 } = {}) {
336
+ const used = new Set((await listNodes()).map((n) => Number(n.port)));
337
+ const listen = (p) => new Promise((resolve) => {
338
+ const srv = net.createServer();
339
+ srv.once('error', () => resolve(false));
340
+ srv.once('listening', () => srv.close(() => resolve(true)));
341
+ srv.listen(p, '127.0.0.1');
342
+ });
343
+ const base = Math.max(1, Math.min(65535, Number(start) || 9188));
344
+ for (let p = base; p < Math.min(65535, base + 200); p++) {
345
+ if (used.has(p)) continue;
346
+ if (await listen(p)) return { port: p, dir: `~/ContextOS-${p}` };
347
+ }
348
+ throw new Error(`no free port after ${base}`);
349
+ }
350
+
351
+ export async function seedProject({ node, avatarUid = '' } = {}) {
352
+ const before = await pmList({ node, avatarUid, lang: 'zh' });
353
+ if (!before.project?.space) throw new Error('avatar has no project');
354
+ if ((before.targets || []).length) return { skipped: true, targets: before.targets.length };
355
+ const make = async (kind, title, content, extra = {}) => pmCreateItem({ node, avatarUid, kind, title, content, ...extra });
356
+ const t1 = await make('target', '个人效率与日常任务', '用数字分身管理每周目标、计划、任务、报告和反馈,形成可复盘的个人工作流。');
357
+ const p1 = await make('plan', '本周执行计划', '完成日常任务并提交日报,周内复盘。', { targetUid: t1.uid, beginTime: new Date().toISOString().slice(0, 10), endTime: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10) });
358
+ const k1 = await make('task', '梳理本周任务清单', '列出本周 5 个关键任务并排优先级。', { planUid: p1.uid });
359
+ const r1 = await make('report', '本周任务清单报告', '任务清单、优先级和完成情况。', { taskUid: k1.uid });
360
+ await make('feedback', '清单反馈', '任务数量合理,建议补充验收标准。', { reportUid: r1.uid, score: '5', feedbackStatus: 'canFinish' });
361
+ const k2 = await make('task', '完成两次深度工作', '每天安排一次 90 分钟深度工作。', { planUid: p1.uid });
362
+ const r2 = await make('report', '深度工作日报', '记录两次深度工作的产出和中断点。', { taskUid: k2.uid });
363
+ await make('feedback', '深度工作反馈', '继续保持,减少会议打断。', { reportUid: r2.uid, score: '4', feedbackStatus: 'canFinish' });
364
+ const t2 = await make('target', 'ContextOS 产品发布', '完成产品演示、交付文档和发布验收。');
365
+ const p2 = await make('plan', '发布准备计划', '准备演示、文档和发布检查清单。', { targetUid: t2.uid, beginTime: new Date().toISOString().slice(0, 10), endTime: new Date(Date.now() + 14 * 86400000).toISOString().slice(0, 10) });
366
+ const k3 = await make('task', '准备产品演示', '整理 10 分钟演示脚本与数据。', { planUid: p2.uid });
367
+ const r3 = await make('report', '演示脚本报告', '脚本、截图和演示环境说明。', { taskUid: k3.uid });
368
+ await make('feedback', '演示反馈', '演示路径清晰,补充异常场景。', { reportUid: r3.uid, score: '5', feedbackStatus: 'canFinish' });
369
+ const t3 = await make('target', '赛博世界场景验证', '验证场景切换、背景与设备发现联动。');
370
+ const p3 = await make('plan', '设备接入验证', '检查 WiFi、蓝牙和 ContextOS 节点发现。', { targetUid: t3.uid, beginTime: new Date().toISOString().slice(0, 10), endTime: new Date(Date.now() + 10 * 86400000).toISOString().slice(0, 10) });
371
+ const k4 = await make('task', '发现周边设备', '记录设备清单与连接方式。', { planUid: p3.uid });
372
+ const r4 = await make('report', '设备发现报告', '设备类型、数量和可控性说明。', { taskUid: k4.uid });
373
+ await make('feedback', '设备发现反馈', '发现能力可用,控制能力待补齐。', { reportUid: r4.uid, score: '4', feedbackStatus: 'shouldImprove' });
374
+ const t4 = await make('target', '客户交付与验收', '把交付、验收、整改串成一条可追溯的链路。');
375
+ const p4 = await make('plan', '交付准备', '清单、环境、演示脚本一次备齐。', { targetUid: t4.uid, beginTime: new Date().toISOString().slice(0, 10), endTime: new Date(Date.now() + 10 * 86400000).toISOString().slice(0, 10) });
376
+ const k5 = await make('task', '整理交付清单', '列出交付物、责任人和验收标准。', { planUid: p4.uid });
377
+ const r5 = await make('report', '交付清单周报', '本周完成 8 项交付物梳理,2 项待确认。', { taskUid: k5.uid, reportType: 'weekly' });
378
+ await make('feedback', '交付清单反馈', '清单完整,验收标准需量化。', { reportUid: r5.uid, score: '5', feedbackStatus: 'canFinish' });
379
+ const k6 = await make('task', '客户验收演示', '10 分钟演示脚本与现场问答。', { planUid: p4.uid });
380
+ const r6 = await make('report', '验收演示季报', '演示通过,客户提出 3 条整改意见。', { taskUid: k6.uid, reportType: 'quarterly' });
381
+ await make('feedback', '演示反馈', '节奏偏快,异常场景要补。', { reportUid: r6.uid, score: '3', feedbackStatus: 'shouldImprove' });
382
+ return { skipped: false, targets: 4, plans: 4, tasks: 6, reports: 6, feedbacks: 6 };
383
+ }
384
+
385
+ export async function projectStatus({ node, avatarUid = '' } = {}) {
386
+ const tree = await pmList({ node, avatarUid, lang: 'zh' });
387
+ if (!tree.avatar) throw new Error('avatar not found');
388
+ return {
389
+ avatar: tree.avatar,
390
+ project: tree.project,
391
+ counts: { target: String(tree.targets.length), plan: String(tree.plans.length), task: String(tree.tasks.length), report: String(tree.reports.length), feedback: String(tree.feedbacks.length) },
392
+ };
393
+ }