@tom2012/cc-web 1.5.83 → 1.5.85

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 (53) hide show
  1. package/README.md +1 -1
  2. package/backend/dist/config.d.ts +2 -0
  3. package/backend/dist/config.d.ts.map +1 -1
  4. package/backend/dist/config.js +1 -0
  5. package/backend/dist/config.js.map +1 -1
  6. package/backend/dist/index.d.ts.map +1 -1
  7. package/backend/dist/index.js +27 -1
  8. package/backend/dist/index.js.map +1 -1
  9. package/backend/dist/plan-control/checker.d.ts +3 -0
  10. package/backend/dist/plan-control/checker.d.ts.map +1 -0
  11. package/backend/dist/plan-control/checker.js +103 -0
  12. package/backend/dist/plan-control/checker.js.map +1 -0
  13. package/backend/dist/plan-control/executor.d.ts +85 -0
  14. package/backend/dist/plan-control/executor.d.ts.map +1 -0
  15. package/backend/dist/plan-control/executor.js +854 -0
  16. package/backend/dist/plan-control/executor.js.map +1 -0
  17. package/backend/dist/plan-control/parser.d.ts +12 -0
  18. package/backend/dist/plan-control/parser.d.ts.map +1 -0
  19. package/backend/dist/plan-control/parser.js +216 -0
  20. package/backend/dist/plan-control/parser.js.map +1 -0
  21. package/backend/dist/plan-control/templates.d.ts +4 -0
  22. package/backend/dist/plan-control/templates.d.ts.map +1 -0
  23. package/backend/dist/plan-control/templates.js +218 -0
  24. package/backend/dist/plan-control/templates.js.map +1 -0
  25. package/backend/dist/plan-control/types.d.ts +102 -0
  26. package/backend/dist/plan-control/types.d.ts.map +1 -0
  27. package/backend/dist/plan-control/types.js +12 -0
  28. package/backend/dist/plan-control/types.js.map +1 -0
  29. package/backend/dist/routes/plan-control.d.ts +8 -0
  30. package/backend/dist/routes/plan-control.d.ts.map +1 -0
  31. package/backend/dist/routes/plan-control.js +304 -0
  32. package/backend/dist/routes/plan-control.js.map +1 -0
  33. package/frontend/dist/assets/GraphPreview-2WXuNUTC.js +2 -0
  34. package/frontend/dist/assets/{OfficePreview-CGE86imK.js → OfficePreview-Ddn426ac.js} +2 -2
  35. package/frontend/dist/assets/PlanPanel-CAwoVERS.js +13 -0
  36. package/frontend/dist/assets/{ProjectPage-LZuxFngN.js → ProjectPage-Bsmij9PR.js} +4 -4
  37. package/frontend/dist/assets/{SettingsPage-DcE9vyAO.js → SettingsPage-D_qkFpli.js} +1 -1
  38. package/frontend/dist/assets/{ShareViewPage-RQfo-sAE.js → ShareViewPage-D_qyAmta.js} +1 -1
  39. package/frontend/dist/assets/{SkillHubPage-BuQCOzTY.js → SkillHubPage-C0FH24pf.js} +1 -1
  40. package/frontend/dist/assets/{bot-BTPL5fMo.js → bot-BlJwFT2K.js} +1 -1
  41. package/frontend/dist/assets/{chevron-down-B5QB8OvI.js → chevron-down-CK5p8rdp.js} +1 -1
  42. package/frontend/dist/assets/{download-CRCiN6lp.js → download-CUOp54z9.js} +1 -1
  43. package/frontend/dist/assets/{index-Cq7E361N.js → index-0ybprEWn.js} +1 -1
  44. package/frontend/dist/assets/{index-DxdTRVVE.js → index-BV5oU0la.js} +10 -10
  45. package/frontend/dist/assets/index-C8OaV-rT.css +1 -0
  46. package/frontend/dist/assets/{jszip.min-86jXs0Jp.js → jszip.min-5kkrvu4H.js} +1 -1
  47. package/frontend/dist/assets/maximize-2-CjNiqJGr.js +7 -0
  48. package/frontend/dist/assets/{save-VwSCjXJc.js → save-DFFb1Eaz.js} +1 -1
  49. package/frontend/dist/assets/{user-B-E27M2h.js → user-JyvDYJ0V.js} +1 -1
  50. package/frontend/dist/index.html +2 -2
  51. package/package.json +1 -1
  52. package/frontend/dist/assets/GraphPreview-DeUOTqGU.js +0 -8
  53. package/frontend/dist/assets/index-Cko3_F0X.css +0 -1
@@ -0,0 +1,854 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PlanExecutor = void 0;
37
+ // backend/src/plan-control/executor.ts
38
+ const events_1 = require("events");
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const config_1 = require("../config");
42
+ const types_1 = require("./types");
43
+ const parser_1 = require("./parser");
44
+ const checker_1 = require("./checker");
45
+ class PlanExecutor extends events_1.EventEmitter {
46
+ constructor(projectPath, deps, config) {
47
+ super();
48
+ this.state = null;
49
+ this.ast = [];
50
+ this.funcs = new Map();
51
+ this.flatNodes = []; // all non-blank/comment nodes in order
52
+ this.machineState = 'STOPPED';
53
+ this.nodeCounter = 0;
54
+ this.currentNodeId = null;
55
+ this.fileWatcher = null;
56
+ this.pollTimer = null;
57
+ this.idleCheckTimer = null;
58
+ this.nudgeTimer = null;
59
+ this.nudgeCount = 0;
60
+ this.preReplanLines = null;
61
+ this.pendingPause = false;
62
+ this.projectPath = projectPath;
63
+ this.pcDir = path.join(projectPath, '.plan-control');
64
+ this.deps = deps;
65
+ this.config = { ...types_1.DEFAULT_PLAN_CONFIG, ...config };
66
+ }
67
+ // ── Public API ──
68
+ checkSyntax() {
69
+ const source = this.readMainPc();
70
+ if (!source)
71
+ return { errors: [{ line: 0, message: 'main.pc 文件不存在' }], ast: [] };
72
+ const ast = (0, parser_1.parseProgram)(source);
73
+ const errors = (0, checker_1.check)(ast);
74
+ return { errors, ast };
75
+ }
76
+ start() {
77
+ const { errors, ast } = this.checkSyntax();
78
+ if (errors.length > 0)
79
+ throw new Error('语法检查失败,请先修复错误');
80
+ this.ast = ast;
81
+ this.funcs = (0, parser_1.collectFuncs)(ast);
82
+ this.flattenAst();
83
+ this.nodeCounter = 0;
84
+ this.state = {
85
+ status: 'running',
86
+ current_line: this.findFirstExecutableLine(),
87
+ executed_tasks: 0,
88
+ estimated_tasks: (0, parser_1.estimateTasks)(ast),
89
+ variables: {},
90
+ call_stack: [],
91
+ loop_stack: [],
92
+ last_task_status: null,
93
+ history: [],
94
+ };
95
+ this.processInitialVarAssigns();
96
+ this.saveState();
97
+ this.machineState = 'IDLE';
98
+ this.broadcastStatus();
99
+ this.tick();
100
+ }
101
+ resume() {
102
+ if (!this.state) {
103
+ this.tryRecoverState();
104
+ if (!this.state)
105
+ throw new Error('无执行状态可恢复');
106
+ }
107
+ const source = this.readMainPc();
108
+ if (!source)
109
+ throw new Error('main.pc 不存在');
110
+ this.ast = (0, parser_1.parseProgram)(source);
111
+ this.funcs = (0, parser_1.collectFuncs)(this.ast);
112
+ this.flattenAst();
113
+ if (this.state.history.length > 0) {
114
+ const maxId = Math.max(...this.state.history.map(h => parseInt(h.node_id, 10)));
115
+ this.nodeCounter = maxId;
116
+ }
117
+ this.state.status = 'running';
118
+ this.saveState();
119
+ this.machineState = 'IDLE';
120
+ this.pendingPause = false;
121
+ this.broadcastStatus();
122
+ this.tick();
123
+ }
124
+ pause() {
125
+ if (this.machineState === 'WAITING') {
126
+ this.pendingPause = true;
127
+ return;
128
+ }
129
+ this.enterPaused('用户暂停');
130
+ }
131
+ stop() {
132
+ this.cleanup();
133
+ if (this.state) {
134
+ this.state.status = 'stopped';
135
+ this.state.stop_line = this.state.current_line;
136
+ this.state.stop_node_id = this.currentNodeId ?? undefined;
137
+ this.saveState();
138
+ }
139
+ this.machineState = 'STOPPED';
140
+ this.broadcastStatus();
141
+ }
142
+ getState() {
143
+ return this.state;
144
+ }
145
+ getNodes() {
146
+ const nodesDir = path.join(this.pcDir, 'nodes');
147
+ if (!fs.existsSync(nodesDir))
148
+ return [];
149
+ return fs.readdirSync(nodesDir)
150
+ .filter(f => f.startsWith('node-') && f.endsWith('.json'))
151
+ .sort()
152
+ .map(f => {
153
+ try {
154
+ return JSON.parse(fs.readFileSync(path.join(nodesDir, f), 'utf-8'));
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ })
160
+ .filter(Boolean);
161
+ }
162
+ getNode(nodeId) {
163
+ const file = path.join(this.pcDir, 'nodes', `node-${nodeId}.json`);
164
+ if (!fs.existsSync(file))
165
+ return null;
166
+ try {
167
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ }
173
+ isInitialized() {
174
+ return fs.existsSync(this.pcDir) && fs.existsSync(path.join(this.pcDir, 'plan-code.md'));
175
+ }
176
+ hasMainPc() {
177
+ return fs.existsSync(path.join(this.pcDir, 'main.pc'));
178
+ }
179
+ // ── State Machine ──
180
+ tick() {
181
+ if (this.machineState !== 'IDLE' || !this.state)
182
+ return;
183
+ const node = this.findNodeAtLine(this.state.current_line);
184
+ if (!node) {
185
+ this.state.status = 'completed';
186
+ this.saveState();
187
+ this.machineState = 'COMPLETED';
188
+ this.broadcastStatus();
189
+ return;
190
+ }
191
+ if (node.type === 'task' || node.type === 'task_assign') {
192
+ this.dispatchTask(node);
193
+ }
194
+ else {
195
+ this.processControlFlow(node);
196
+ this.advanceLine();
197
+ this.saveState();
198
+ this.broadcastStatus();
199
+ setImmediate(() => this.tick());
200
+ }
201
+ }
202
+ dispatchTask(node) {
203
+ this.machineState = 'SENDING';
204
+ const description = node.description ?? '';
205
+ const resolvedDesc = (0, parser_1.interpolate)(description, this.getEffectiveVariables());
206
+ const nodeId = this.nextNodeId();
207
+ this.currentNodeId = nodeId;
208
+ const resolvedCode = node.type === 'task_assign'
209
+ ? `${node.varName} = task ${resolvedDesc}`
210
+ : `task ${resolvedDesc}`;
211
+ const prompt = this.buildPrompt(nodeId, resolvedDesc, node);
212
+ const record = {
213
+ id: nodeId,
214
+ line: node.line,
215
+ code: node.raw,
216
+ resolved_code: resolvedCode,
217
+ prompt,
218
+ started_at: new Date().toISOString(),
219
+ completed_at: null,
220
+ nudge_count: 0,
221
+ status: null,
222
+ result: null,
223
+ summary: null,
224
+ };
225
+ const nodesDir = path.join(this.pcDir, 'nodes');
226
+ fs.mkdirSync(nodesDir, { recursive: true });
227
+ fs.writeFileSync(path.join(nodesDir, `node-${nodeId}.json`), JSON.stringify(record, null, 2));
228
+ this.waitForIdle(() => {
229
+ this.deps.writeToPty(prompt + '\n');
230
+ this.machineState = 'WAITING';
231
+ this.state.status = 'waiting';
232
+ this.saveState();
233
+ this.broadcastStatus();
234
+ this.startFileWatch(nodeId);
235
+ this.startNudgeTimer(nodeId);
236
+ });
237
+ }
238
+ processControlFlow(node) {
239
+ const state = this.state;
240
+ switch (node.type) {
241
+ case 'var_assign':
242
+ if (node.varName && node.listItems) {
243
+ this.setVariable(node.varName, node.listItems);
244
+ }
245
+ break;
246
+ case 'if': {
247
+ const matched = this.evaluateCondition(node.condition);
248
+ if (!matched) {
249
+ this.skipIfChain(node);
250
+ return;
251
+ }
252
+ break;
253
+ }
254
+ case 'elif': {
255
+ this.skipIfChain(node);
256
+ return;
257
+ }
258
+ case 'else': {
259
+ break;
260
+ }
261
+ case 'for': {
262
+ const vars = this.getEffectiveVariables();
263
+ const listVal = vars[node.iterRef];
264
+ let list;
265
+ if (Array.isArray(listVal)) {
266
+ list = listVal;
267
+ }
268
+ else if (typeof listVal === 'string') {
269
+ list = [listVal];
270
+ }
271
+ else if (listVal === null || listVal === undefined || typeof listVal === 'boolean') {
272
+ this.enterPaused(`for 循环变量 \${${node.iterRef}} 不是列表(值: ${JSON.stringify(listVal)})`);
273
+ return;
274
+ }
275
+ else {
276
+ list = [];
277
+ }
278
+ if (list.length === 0) {
279
+ this.skipBlock(node);
280
+ return;
281
+ }
282
+ const endLine = this.findBlockEndLine(node);
283
+ state.loop_stack.push({
284
+ type: 'for',
285
+ var: node.iterVar,
286
+ list,
287
+ index: 0,
288
+ start_line: node.line,
289
+ end_line: endLine,
290
+ });
291
+ this.setVariable(node.iterVar, list[0]);
292
+ break;
293
+ }
294
+ case 'loop': {
295
+ const endLine = this.findBlockEndLine(node);
296
+ state.loop_stack.push({
297
+ type: 'loop',
298
+ var: node.loopCounter,
299
+ count: node.loopCount,
300
+ index: 0,
301
+ start_line: node.line,
302
+ end_line: endLine,
303
+ });
304
+ if (node.loopCounter) {
305
+ this.setVariable(node.loopCounter, '1');
306
+ }
307
+ break;
308
+ }
309
+ case 'break': {
310
+ const frame = state.loop_stack.pop();
311
+ if (frame) {
312
+ state.current_line = frame.end_line;
313
+ }
314
+ break;
315
+ }
316
+ case 'continue': {
317
+ const frame = state.loop_stack[state.loop_stack.length - 1];
318
+ if (frame) {
319
+ frame.index++;
320
+ if (this.isLoopDone(frame)) {
321
+ state.loop_stack.pop();
322
+ state.current_line = frame.end_line;
323
+ }
324
+ else {
325
+ this.updateLoopVar(frame);
326
+ state.current_line = frame.start_line;
327
+ }
328
+ }
329
+ break;
330
+ }
331
+ case 'func':
332
+ this.skipBlock(node);
333
+ return;
334
+ case 'call': {
335
+ if (state.call_stack.length >= 20) {
336
+ this.enterPaused('call_stack 深度超过限制 (20)');
337
+ return;
338
+ }
339
+ const funcNode = this.funcs.get(node.funcName);
340
+ if (!funcNode)
341
+ break;
342
+ const vars = this.getEffectiveVariables();
343
+ const frame = {
344
+ func: node.funcName,
345
+ return_line: this.findNextLineAfter(node.line),
346
+ local_vars: {},
347
+ saved_last_task_status: state.last_task_status,
348
+ };
349
+ if (funcNode.params && node.args) {
350
+ for (let i = 0; i < funcNode.params.length; i++) {
351
+ const param = funcNode.params[i];
352
+ const arg = node.args[i];
353
+ if (arg) {
354
+ if (arg.type === 'var') {
355
+ frame.local_vars[param] = vars[arg.name] ?? null;
356
+ }
357
+ else {
358
+ frame.local_vars[param] = arg.items;
359
+ }
360
+ }
361
+ }
362
+ }
363
+ state.call_stack.push(frame);
364
+ state.current_line = funcNode.line;
365
+ break;
366
+ }
367
+ case 'return': {
368
+ const callFrame = state.call_stack.pop();
369
+ if (callFrame) {
370
+ state.last_task_status = callFrame.saved_last_task_status;
371
+ state.current_line = callFrame.return_line - 1;
372
+ }
373
+ break;
374
+ }
375
+ case 'comment':
376
+ case 'blank':
377
+ break;
378
+ }
379
+ }
380
+ // ── File Watching ──
381
+ startFileWatch(nodeId) {
382
+ const nodeFile = path.join(this.pcDir, 'nodes', `node-${nodeId}.json`);
383
+ this.nudgeCount = 0;
384
+ try {
385
+ this.fileWatcher = fs.watch(nodeFile, () => this.checkNodeResult(nodeId));
386
+ }
387
+ catch { /* fs.watch may not work on all platforms */ }
388
+ this.pollTimer = setInterval(() => this.checkNodeResult(nodeId), this.config.watch_poll_interval);
389
+ }
390
+ checkNodeResult(nodeId) {
391
+ if (this.machineState !== 'WAITING')
392
+ return;
393
+ const record = this.getNode(nodeId);
394
+ if (!record || record.status === null)
395
+ return;
396
+ const validStatuses = ['success', 'failed', 'blocked', 'replan'];
397
+ if (!validStatuses.includes(record.status)) {
398
+ this.deps.writeToPty(`\n[PLAN-CONTROL] 错误:status 值 "${record.status}" 无效。有效值: ${validStatuses.join(', ')}\n`);
399
+ return;
400
+ }
401
+ this.cleanup();
402
+ record.completed_at = new Date().toISOString();
403
+ fs.writeFileSync(path.join(this.pcDir, 'nodes', `node-${nodeId}.json`), JSON.stringify(record, null, 2));
404
+ this.processResult(record);
405
+ }
406
+ processResult(record) {
407
+ if (!this.state)
408
+ return;
409
+ this.machineState = 'PROCESSING';
410
+ const node = this.findNodeAtLine(this.state.current_line);
411
+ if (node?.type === 'task_assign' && node.varName) {
412
+ this.setVariable(node.varName, record.result);
413
+ }
414
+ const normalizedStatus = record.status === 'replan' ? 'success' : record.status;
415
+ this.state.last_task_status = normalizedStatus;
416
+ this.state.executed_tasks++;
417
+ this.state.history.push({
418
+ node_id: record.id,
419
+ line: record.line,
420
+ status: record.status,
421
+ timestamp: record.completed_at,
422
+ });
423
+ this.deps.broadcast({
424
+ type: 'plan_node_update',
425
+ node_id: record.id,
426
+ status: record.status,
427
+ summary: record.summary,
428
+ });
429
+ const needsReplan = record.request_replan || record.status === 'replan';
430
+ if (needsReplan) {
431
+ this.enterReplanning(record);
432
+ return;
433
+ }
434
+ if (this.pendingPause) {
435
+ this.pendingPause = false;
436
+ this.enterPaused('用户暂停');
437
+ return;
438
+ }
439
+ this.advanceLine();
440
+ this.state.status = 'running';
441
+ this.saveState();
442
+ this.machineState = 'IDLE';
443
+ this.broadcastStatus();
444
+ this.tick();
445
+ }
446
+ // ── Nudge ──
447
+ startNudgeTimer(nodeId) {
448
+ const interval = this.config.nudge_idle_seconds * 1000 *
449
+ Math.pow(this.config.nudge_interval_multiplier, this.nudgeCount);
450
+ this.nudgeTimer = setTimeout(() => {
451
+ if (this.machineState !== 'WAITING')
452
+ return;
453
+ const lastActivity = this.deps.getLastActivity();
454
+ const now = Date.now();
455
+ const idle = lastActivity ? (now - lastActivity) > this.config.nudge_idle_seconds * 1000 : true;
456
+ if (!idle) {
457
+ this.startNudgeTimer(nodeId);
458
+ return;
459
+ }
460
+ this.nudgeCount++;
461
+ if (this.nudgeCount > this.config.nudge_max_count) {
462
+ const record = this.getNode(nodeId);
463
+ if (record && !record.status) {
464
+ record.status = 'blocked';
465
+ record.summary = `${this.config.nudge_max_count} 次催促后仍无响应`;
466
+ record.completed_at = new Date().toISOString();
467
+ fs.writeFileSync(path.join(this.pcDir, 'nodes', `node-${nodeId}.json`), JSON.stringify(record, null, 2));
468
+ this.cleanup();
469
+ this.processResult(record);
470
+ }
471
+ return;
472
+ }
473
+ this.deps.writeToPty(`\n请继续当前任务。如果已完成,请按照 .plan-control/output-format.md 的格式更新 .plan-control/nodes/node-${nodeId}.json\n`);
474
+ const record = this.getNode(nodeId);
475
+ if (record) {
476
+ record.nudge_count = this.nudgeCount;
477
+ fs.writeFileSync(path.join(this.pcDir, 'nodes', `node-${nodeId}.json`), JSON.stringify(record, null, 2));
478
+ }
479
+ this.deps.broadcast({ type: 'plan_nudge', node_id: nodeId, nudge_count: this.nudgeCount });
480
+ this.startNudgeTimer(nodeId);
481
+ }, interval);
482
+ }
483
+ // ── Replan ──
484
+ enterReplanning(record) {
485
+ if (!this.state)
486
+ return;
487
+ this.state.status = 'replanning';
488
+ this.saveState();
489
+ this.machineState = 'REPLANNING';
490
+ const source = this.readMainPc();
491
+ this.preReplanLines = source ? source.split('\n') : [];
492
+ const prompt = `\n[PLAN-CONTROL] 计划调整请求
493
+ 原因:${record.replan_reason || '未提供原因'}
494
+ 请修改 .plan-control/main.pc,保留前 ${this.state.current_line} 行不变,调整后续计划。
495
+ 修改完成后,请更新 .plan-control/nodes/node-${record.id}.json 的 status 为 "success"。
496
+ [/PLAN-CONTROL]\n`;
497
+ this.deps.writeToPty(prompt);
498
+ this.deps.broadcast({ type: 'plan_replan', node_id: record.id, reason: record.replan_reason });
499
+ this.broadcastStatus();
500
+ this.startFileWatch(record.id);
501
+ }
502
+ handleReplanComplete(nodeId) {
503
+ if (!this.state || !this.preReplanLines)
504
+ return;
505
+ const newSource = this.readMainPc();
506
+ if (!newSource) {
507
+ this.deps.writeToPty('\n[PLAN-CONTROL] 错误:main.pc 不存在\n');
508
+ return;
509
+ }
510
+ const newLines = newSource.split('\n');
511
+ const preserveCount = this.state.current_line;
512
+ for (let i = 0; i < preserveCount && i < this.preReplanLines.length; i++) {
513
+ if (newLines[i] !== this.preReplanLines[i]) {
514
+ this.deps.writeToPty(`\n[PLAN-CONTROL] 错误:前 ${preserveCount} 行不可修改(第 ${i + 1} 行已变更),请恢复并仅修改第 ${preserveCount + 1} 行之后的内容\n`);
515
+ return;
516
+ }
517
+ }
518
+ const ast = (0, parser_1.parseProgram)(newSource);
519
+ const errors = (0, checker_1.check)(ast);
520
+ if (errors.length > 0) {
521
+ const errText = errors.map(e => `第 ${e.line} 行: ${e.message}`).join('\n');
522
+ this.deps.writeToPty(`\n[PLAN-CONTROL] 语法错误:\n${errText}\n请修复后重试。\n`);
523
+ return;
524
+ }
525
+ this.preReplanLines = null;
526
+ this.ast = ast;
527
+ this.funcs = (0, parser_1.collectFuncs)(ast);
528
+ this.flattenAst();
529
+ this.state.estimated_tasks = (0, parser_1.estimateTasks)(ast);
530
+ this.state.status = 'running';
531
+ this.advanceLine();
532
+ this.saveState();
533
+ this.machineState = 'IDLE';
534
+ this.broadcastStatus();
535
+ this.tick();
536
+ }
537
+ // ── Crash Recovery ──
538
+ tryRecoverState() {
539
+ const stateFile = path.join(this.pcDir, 'state.json');
540
+ if (!fs.existsSync(stateFile))
541
+ return;
542
+ try {
543
+ this.state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
544
+ }
545
+ catch {
546
+ return;
547
+ }
548
+ if (!this.state || this.state.status === 'completed') {
549
+ this.state = null;
550
+ return;
551
+ }
552
+ const source = this.readMainPc();
553
+ if (!source)
554
+ return;
555
+ this.ast = (0, parser_1.parseProgram)(source);
556
+ this.funcs = (0, parser_1.collectFuncs)(this.ast);
557
+ this.flattenAst();
558
+ if (this.state.history.length > 0) {
559
+ this.nodeCounter = Math.max(...this.state.history.map(h => parseInt(h.node_id, 10)));
560
+ }
561
+ }
562
+ // ── Helpers ──
563
+ readMainPc() {
564
+ const file = path.join(this.pcDir, 'main.pc');
565
+ if (!fs.existsSync(file))
566
+ return null;
567
+ return fs.readFileSync(file, 'utf-8');
568
+ }
569
+ saveState() {
570
+ if (!this.state)
571
+ return;
572
+ fs.mkdirSync(this.pcDir, { recursive: true });
573
+ (0, config_1.atomicWriteSync)(path.join(this.pcDir, 'state.json'), JSON.stringify(this.state, null, 2));
574
+ }
575
+ nextNodeId() {
576
+ this.nodeCounter++;
577
+ return String(this.nodeCounter).padStart(3, '0');
578
+ }
579
+ getEffectiveVariables() {
580
+ if (!this.state)
581
+ return {};
582
+ const vars = { ...this.state.variables };
583
+ if (this.state.call_stack.length > 0) {
584
+ const frame = this.state.call_stack[this.state.call_stack.length - 1];
585
+ Object.assign(vars, frame.local_vars);
586
+ }
587
+ return vars;
588
+ }
589
+ setVariable(name, value) {
590
+ if (!this.state)
591
+ return;
592
+ if (this.state.call_stack.length > 0) {
593
+ this.state.call_stack[this.state.call_stack.length - 1].local_vars[name] = value;
594
+ }
595
+ else {
596
+ this.state.variables[name] = value;
597
+ }
598
+ }
599
+ evaluateCondition(condition) {
600
+ if (!this.state)
601
+ return false;
602
+ if (['success', 'failed', 'blocked'].includes(condition)) {
603
+ return this.state.last_task_status === condition;
604
+ }
605
+ const varMatch = condition.match(/^\$\{(.+)\}$/);
606
+ if (varMatch) {
607
+ const val = this.getEffectiveVariables()[varMatch[1]];
608
+ if (val === null || val === undefined || val === false)
609
+ return false;
610
+ if (val === '')
611
+ return false;
612
+ if (Array.isArray(val) && val.length === 0)
613
+ return false;
614
+ return true;
615
+ }
616
+ return false;
617
+ }
618
+ findNodeAtLine(line) {
619
+ return this.flatNodes.find(n => n.line === line);
620
+ }
621
+ flattenAst() {
622
+ this.flatNodes = [];
623
+ const walk = (nodes) => {
624
+ for (const n of nodes) {
625
+ if (n.type !== 'blank' && n.type !== 'comment') {
626
+ this.flatNodes.push(n);
627
+ }
628
+ if (n.children.length > 0)
629
+ walk(n.children);
630
+ }
631
+ };
632
+ walk(this.ast);
633
+ this.flatNodes.sort((a, b) => a.line - b.line);
634
+ }
635
+ findFirstExecutableLine() {
636
+ for (const n of this.flatNodes) {
637
+ if (n.type !== 'blank' && n.type !== 'comment')
638
+ return n.line;
639
+ }
640
+ return 1;
641
+ }
642
+ advanceLine() {
643
+ if (!this.state)
644
+ return;
645
+ let currentIdx = this.flatNodes.findIndex(n => n.line === this.state.current_line);
646
+ // current_line may point to a blank/comment line (not in flatNodes).
647
+ // Find the next flatNode after current_line instead of jumping to end.
648
+ if (currentIdx < 0) {
649
+ const nextIdx = this.flatNodes.findIndex(n => n.line > this.state.current_line);
650
+ if (nextIdx >= 0) {
651
+ this.state.current_line = this.flatNodes[nextIdx].line;
652
+ this.checkLoopEnd();
653
+ this.checkCallEnd();
654
+ return;
655
+ }
656
+ if (this.checkLoopEnd())
657
+ return;
658
+ this.state.current_line = this.flatNodes.length > 0
659
+ ? this.flatNodes[this.flatNodes.length - 1].line + 1
660
+ : 1;
661
+ return;
662
+ }
663
+ if (currentIdx + 1 >= this.flatNodes.length) {
664
+ if (this.checkLoopEnd())
665
+ return;
666
+ this.state.current_line = this.flatNodes[this.flatNodes.length - 1].line + 1;
667
+ return;
668
+ }
669
+ const nextNode = this.flatNodes[currentIdx + 1];
670
+ this.state.current_line = nextNode.line;
671
+ this.checkLoopEnd();
672
+ this.checkCallEnd();
673
+ }
674
+ checkLoopEnd() {
675
+ if (!this.state || this.state.loop_stack.length === 0)
676
+ return false;
677
+ const frame = this.state.loop_stack[this.state.loop_stack.length - 1];
678
+ if (this.state.current_line > frame.end_line) {
679
+ frame.index++;
680
+ if (this.isLoopDone(frame)) {
681
+ this.state.loop_stack.pop();
682
+ return false;
683
+ }
684
+ this.updateLoopVar(frame);
685
+ this.state.current_line = frame.start_line;
686
+ const loopNode = this.findNodeAtLine(frame.start_line);
687
+ if (loopNode && loopNode.children.length > 0) {
688
+ this.state.current_line = loopNode.children[0].line;
689
+ }
690
+ return true;
691
+ }
692
+ return false;
693
+ }
694
+ /** Implicit return: if current_line left the function body, pop the call frame. */
695
+ checkCallEnd() {
696
+ if (!this.state || this.state.call_stack.length === 0)
697
+ return;
698
+ const frame = this.state.call_stack[this.state.call_stack.length - 1];
699
+ const funcNode = this.funcs.get(frame.func);
700
+ if (!funcNode)
701
+ return;
702
+ const endLine = this.findBlockEndLine(funcNode);
703
+ if (this.state.current_line > endLine) {
704
+ this.state.call_stack.pop();
705
+ this.state.last_task_status = frame.saved_last_task_status;
706
+ this.state.current_line = frame.return_line;
707
+ }
708
+ }
709
+ isLoopDone(frame) {
710
+ if (frame.type === 'for') {
711
+ return frame.index >= (frame.list?.length ?? 0);
712
+ }
713
+ return frame.index >= (frame.count ?? 0);
714
+ }
715
+ updateLoopVar(frame) {
716
+ if (frame.type === 'for' && frame.var && frame.list) {
717
+ this.setVariable(frame.var, frame.list[frame.index]);
718
+ }
719
+ else if (frame.type === 'loop' && frame.var) {
720
+ this.setVariable(frame.var, String(frame.index + 1));
721
+ }
722
+ }
723
+ skipBlock(node) {
724
+ if (!this.state)
725
+ return;
726
+ const endLine = this.findBlockEndLine(node);
727
+ this.state.current_line = endLine;
728
+ }
729
+ skipIfChain(node) {
730
+ if (!this.state)
731
+ return;
732
+ const siblings = this.findSiblings(node);
733
+ if (!siblings)
734
+ return;
735
+ const idx = siblings.findIndex(s => s.line === node.line);
736
+ let lastInChain = idx;
737
+ for (let i = idx + 1; i < siblings.length; i++) {
738
+ if (siblings[i].type === 'elif' || siblings[i].type === 'else') {
739
+ lastInChain = i;
740
+ }
741
+ else
742
+ break;
743
+ }
744
+ const lastNode = siblings[lastInChain];
745
+ const endLine = this.findBlockEndLine(lastNode);
746
+ this.state.current_line = endLine;
747
+ }
748
+ findSiblings(node) {
749
+ const find = (nodes) => {
750
+ for (const n of nodes) {
751
+ if (n.line === node.line)
752
+ return nodes;
753
+ const found = find(n.children);
754
+ if (found)
755
+ return found;
756
+ }
757
+ return null;
758
+ };
759
+ return find(this.ast);
760
+ }
761
+ findBlockEndLine(node) {
762
+ if (node.children.length === 0)
763
+ return node.line;
764
+ const lastChild = node.children[node.children.length - 1];
765
+ if (lastChild.children.length > 0)
766
+ return this.findBlockEndLine(lastChild);
767
+ return lastChild.line;
768
+ }
769
+ findNextLineAfter(line) {
770
+ const idx = this.flatNodes.findIndex(n => n.line === line);
771
+ if (idx < 0 || idx + 1 >= this.flatNodes.length)
772
+ return line + 1;
773
+ return this.flatNodes[idx + 1].line;
774
+ }
775
+ processInitialVarAssigns() {
776
+ for (const node of this.ast) {
777
+ if (node.type === 'var_assign' && node.varName && node.listItems) {
778
+ this.state.variables[node.varName] = node.listItems;
779
+ }
780
+ }
781
+ }
782
+ waitForIdle(callback) {
783
+ const check = () => {
784
+ const lastActivity = this.deps.getLastActivity();
785
+ const now = Date.now();
786
+ const idle = !lastActivity || (now - lastActivity) > this.config.send_idle_seconds * 1000;
787
+ if (idle) {
788
+ callback();
789
+ }
790
+ else {
791
+ setTimeout(check, 5000);
792
+ }
793
+ };
794
+ check();
795
+ }
796
+ buildPrompt(nodeId, resolvedDesc, node) {
797
+ const state = this.state;
798
+ let prompt = `[PLAN-CONTROL] 任务 #${nodeId}
799
+ 指令:${resolvedDesc}
800
+ 上下文:第${state.executed_tasks + 1}个任务(已完成${state.executed_tasks}个)`;
801
+ if (state.history.length > 0) {
802
+ const prev = state.history[state.history.length - 1];
803
+ prompt += `\n前置任务:#${prev.node_id} (${prev.status})`;
804
+ }
805
+ prompt += `\n输出文件:.plan-control/nodes/node-${nodeId}.json`;
806
+ prompt += `\n输出格式:见 .plan-control/output-format.md`;
807
+ if (node.type === 'task_assign') {
808
+ prompt += `\n返回要求:result 字段请填写为字符串列表、字符串或布尔值`;
809
+ }
810
+ prompt += '\n[/PLAN-CONTROL]';
811
+ return prompt;
812
+ }
813
+ enterPaused(reason) {
814
+ if (!this.state)
815
+ return;
816
+ this.cleanup();
817
+ this.state.status = 'paused';
818
+ this.state.error = reason;
819
+ this.saveState();
820
+ this.machineState = 'PAUSED';
821
+ this.broadcastStatus();
822
+ }
823
+ cleanup() {
824
+ if (this.fileWatcher) {
825
+ this.fileWatcher.close();
826
+ this.fileWatcher = null;
827
+ }
828
+ if (this.pollTimer) {
829
+ clearInterval(this.pollTimer);
830
+ this.pollTimer = null;
831
+ }
832
+ if (this.idleCheckTimer) {
833
+ clearInterval(this.idleCheckTimer);
834
+ this.idleCheckTimer = null;
835
+ }
836
+ if (this.nudgeTimer) {
837
+ clearTimeout(this.nudgeTimer);
838
+ this.nudgeTimer = null;
839
+ }
840
+ }
841
+ broadcastStatus() {
842
+ if (!this.state)
843
+ return;
844
+ this.deps.broadcast({
845
+ type: 'plan_status',
846
+ status: this.state.status,
847
+ executed_tasks: this.state.executed_tasks,
848
+ estimated_tasks: this.state.estimated_tasks,
849
+ current_line: this.state.current_line,
850
+ });
851
+ }
852
+ }
853
+ exports.PlanExecutor = PlanExecutor;
854
+ //# sourceMappingURL=executor.js.map