@fieldwangai/agentflow 0.1.126 → 0.1.129

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.
@@ -105,6 +105,14 @@ export function getPrdWorkflowCollaborationForUser(tapdId, userId) {
105
105
  return records.find((record) => record.ownerId === actorId) || records[0] || null;
106
106
  }
107
107
 
108
+ export function listPrdWorkflowCollaborationsForUser(userId) {
109
+ const actorId = normalizeUserId(userId);
110
+ if (!actorId) return [];
111
+ return Object.values(readRegistry().workflows)
112
+ .filter((record) => prdWorkflowCollaborationAccess(record, actorId).allowed)
113
+ .sort((left, right) => String(right?.updatedAt || "").localeCompare(String(left?.updatedAt || "")));
114
+ }
115
+
108
116
  export function ensurePrdWorkflowCollaboration({ tapdId, userId }) {
109
117
  const ownerId = normalizeUserId(userId);
110
118
  const normalizedTapdId = normalizeTapdId(tapdId);
@@ -156,6 +156,7 @@ import {
156
156
  getPrdWorkflowCollaborationByShareToken,
157
157
  getPrdWorkflowCollaborationForUser,
158
158
  ensurePrdWorkflowShareLink,
159
+ listPrdWorkflowCollaborationsForUser,
159
160
  prdWorkflowCollaborationAccess,
160
161
  prdWorkflowCollaborationSummary,
161
162
  removePrdWorkflowCollaborationMember,
@@ -3349,6 +3350,123 @@ function prdWorkflowShareLinkSummary(record, shareToken, publicBaseUrl, userId =
3349
3350
  };
3350
3351
  }
3351
3352
 
3353
+ function prdWorkflowDashboardActions(snapshot = {}) {
3354
+ const rows = new Map();
3355
+ for (const field of ["actions", "workflowActions", "workflow_actions", "timeline", "history"]) {
3356
+ for (const item of Array.isArray(snapshot?.[field]) ? snapshot[field] : []) {
3357
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
3358
+ const key = String(
3359
+ item.stageKey
3360
+ || item.stage_key
3361
+ || item.actionId
3362
+ || item.action_id
3363
+ || item.id
3364
+ || item.action
3365
+ || "",
3366
+ ).trim();
3367
+ if (!key) continue;
3368
+ rows.set(key, { ...(rows.get(key) || {}), ...item });
3369
+ }
3370
+ }
3371
+ return Array.from(rows.values());
3372
+ }
3373
+
3374
+ function prdWorkflowDashboardTimestamp(item = {}) {
3375
+ for (const value of [
3376
+ item.updatedAt,
3377
+ item.updated_at,
3378
+ item.observedAt,
3379
+ item.observed_at,
3380
+ item.completedAt,
3381
+ item.completed_at,
3382
+ item.stageEnteredAt,
3383
+ item.stage_entered_at,
3384
+ item.actionAt,
3385
+ item.action_at,
3386
+ item.startedAt,
3387
+ item.started_at,
3388
+ item.reportedAt,
3389
+ item.reported_at,
3390
+ item.createdAt,
3391
+ item.created_at,
3392
+ item.at,
3393
+ ]) {
3394
+ const timestamp = Date.parse(String(value || ""));
3395
+ if (Number.isFinite(timestamp)) return timestamp;
3396
+ }
3397
+ return 0;
3398
+ }
3399
+
3400
+ function prdWorkflowDashboardSummary(record, snapshot = {}, userCtx = {}) {
3401
+ const tapdId = String(record?.tapdId || snapshot?.tapdId || snapshot?.tapd_id || "").trim();
3402
+ const collaboration = prdWorkflowCollaborationSummaryWithUsers(record, userCtx?.userId) || {};
3403
+ const actions = prdWorkflowDashboardActions(snapshot);
3404
+ const doneStatuses = new Set(["done", "complete", "success", "completed", "passed", "observed"]);
3405
+ const completedActions = actions.filter((item) => (
3406
+ doneStatuses.has(String(item?.status || "").trim().toLowerCase())
3407
+ )).length;
3408
+ const latestAction = [...actions].sort((left, right) => (
3409
+ prdWorkflowDashboardTimestamp(right) - prdWorkflowDashboardTimestamp(left)
3410
+ ))[0] || null;
3411
+ const issues = Array.isArray(snapshot?.issues)
3412
+ ? snapshot.issues
3413
+ : Array.isArray(snapshot?.raw?.prd?.issues)
3414
+ ? snapshot.raw.prd.issues
3415
+ : [];
3416
+ const platforms = Array.from(new Set(issues
3417
+ .map((issue) => String(issue?.platform || "").trim().toLowerCase())
3418
+ .filter(Boolean)));
3419
+ const phase = String(snapshot?.phase || "").trim();
3420
+ const phaseKey = phase.toLowerCase();
3421
+ const state = /blocked|failed|error|conflict/.test(phaseKey)
3422
+ ? "blocked"
3423
+ : /done|completed|released|closed/.test(phaseKey) || (actions.length > 0 && completedActions === actions.length)
3424
+ ? "completed"
3425
+ : "active";
3426
+ const requirement = snapshot?.overall?.requirement && typeof snapshot.overall.requirement === "object"
3427
+ ? snapshot.overall.requirement
3428
+ : {};
3429
+ const title = String(
3430
+ requirement.title
3431
+ || requirement.name
3432
+ || snapshot?.prd?.title
3433
+ || snapshot?.raw?.prd?.title
3434
+ || snapshot?.title
3435
+ || "",
3436
+ ).trim();
3437
+ const updatedAtTimestamp = Math.max(
3438
+ prdWorkflowDashboardTimestamp(record),
3439
+ prdWorkflowDashboardTimestamp(snapshot),
3440
+ prdWorkflowDashboardTimestamp(latestAction || {}),
3441
+ );
3442
+ return {
3443
+ id: String(record?.id || ""),
3444
+ tapdId,
3445
+ title,
3446
+ phase,
3447
+ state,
3448
+ pointer: String(snapshot?.pointer || "").trim(),
3449
+ revision: String(snapshot?.revision || "").trim(),
3450
+ issueCount: issues.length,
3451
+ platforms,
3452
+ actionCount: actions.length,
3453
+ completedActionCount: completedActions,
3454
+ latestAction: latestAction
3455
+ ? {
3456
+ title: String(latestAction.title || latestAction.label || latestAction.action || latestAction.id || "").trim(),
3457
+ status: String(latestAction.status || "").trim(),
3458
+ at: prdWorkflowDashboardTimestamp(latestAction) ? new Date(prdWorkflowDashboardTimestamp(latestAction)).toISOString() : "",
3459
+ }
3460
+ : null,
3461
+ role: String(collaboration.role || ""),
3462
+ ownerId: String(collaboration.ownerId || ""),
3463
+ ownerUsername: String(collaboration.ownerUsername || collaboration.ownerId || ""),
3464
+ memberCount: Number(collaboration.memberCount || 0),
3465
+ shareActive: collaboration.shareActive === true,
3466
+ updatedAt: updatedAtTimestamp ? new Date(updatedAtTimestamp).toISOString() : String(record?.updatedAt || ""),
3467
+ };
3468
+ }
3469
+
3352
3470
  function workspaceConversationsPath(scopedRoot) {
3353
3471
  return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "conversations.json");
3354
3472
  }
@@ -8372,6 +8490,111 @@ function prdWorkflowReviewRenderPlannedCode(filePath, codeLines, startLine = 0,
8372
8490
  </section>`;
8373
8491
  }
8374
8492
 
8493
+ const PRD_WORKFLOW_VERIFICATION_FIELDS = {
8494
+ "入口": "entry",
8495
+ "前置条件": "precondition",
8496
+ "观察": "observation",
8497
+ "通过标准": "passCriteria",
8498
+ };
8499
+
8500
+ function prdWorkflowReviewVerificationBoundary(line) {
8501
+ const trimmed = String(line || "").trim();
8502
+ return /^#verification\s*$/i.test(trimmed)
8503
+ || /^#change\s+(?:add|modify|remove|move)\s*$/i.test(trimmed)
8504
+ || /^#file\s+.+$/i.test(trimmed)
8505
+ || Boolean(prdWorkflowReviewActionLine(line));
8506
+ }
8507
+
8508
+ function prdWorkflowReviewVerificationContent(lines = []) {
8509
+ const source = Array.isArray(lines) ? [...lines] : [];
8510
+ while (source.length && !String(source[0] || "").trim()) source.shift();
8511
+ while (source.length && !String(source[source.length - 1] || "").trim()) source.pop();
8512
+ if (source.length <= 1) return source;
8513
+ return [source[0], ...prdWorkflowReviewDedentPlannedCode(source.slice(1))];
8514
+ }
8515
+
8516
+ function prdWorkflowReviewParseVerificationBlock(lines, startIndex) {
8517
+ if (!/^\s*#verification\s*$/i.test(String(lines[startIndex] || ""))) return null;
8518
+ const fields = {
8519
+ entry: [],
8520
+ precondition: [],
8521
+ observation: [],
8522
+ passCriteria: [],
8523
+ };
8524
+ let activeField = "";
8525
+ let boundaryIndex = lines.length;
8526
+ for (let i = startIndex + 1; i < lines.length; i += 1) {
8527
+ const line = String(lines[i] || "");
8528
+ if (/^\s*#verificationend\s*$/i.test(line)) {
8529
+ return {
8530
+ verification: {
8531
+ type: "verification",
8532
+ entry: prdWorkflowReviewVerificationContent(fields.entry),
8533
+ precondition: prdWorkflowReviewVerificationContent(fields.precondition),
8534
+ observation: prdWorkflowReviewVerificationContent(fields.observation),
8535
+ passCriteria: prdWorkflowReviewVerificationContent(fields.passCriteria),
8536
+ },
8537
+ complete: true,
8538
+ endIndex: i,
8539
+ };
8540
+ }
8541
+ if (prdWorkflowReviewVerificationBoundary(line)) {
8542
+ boundaryIndex = i;
8543
+ break;
8544
+ }
8545
+ const field = line.match(/^\s*-\s*(入口|前置条件|观察|通过标准)\s*[::]\s*(.*)$/);
8546
+ if (field) {
8547
+ activeField = PRD_WORKFLOW_VERIFICATION_FIELDS[field[1]];
8548
+ if (field[2]) fields[activeField].push(field[2]);
8549
+ continue;
8550
+ }
8551
+ if (activeField) fields[activeField].push(line);
8552
+ }
8553
+ return {
8554
+ verification: null,
8555
+ complete: false,
8556
+ endIndex: boundaryIndex - 1,
8557
+ rawLines: lines.slice(startIndex + 1, boundaryIndex),
8558
+ };
8559
+ }
8560
+
8561
+ function prdWorkflowReviewRenderVerificationBlock(verification) {
8562
+ const fields = [
8563
+ ["入口", verification.entry],
8564
+ ["前置条件", verification.precondition],
8565
+ ["观察", verification.observation],
8566
+ ["通过标准", verification.passCriteria],
8567
+ ];
8568
+ const rows = fields.map(([label, content]) => {
8569
+ const missing = !Array.isArray(content) || !content.some((line) => String(line || "").trim());
8570
+ const value = missing
8571
+ ? '<span class="verification-block__missing">未填写</span>'
8572
+ : prdWorkflowReviewMarkdownLinesToHtml(content);
8573
+ return `<div class="verification-block__field${missing ? " is-missing" : ""}">
8574
+ <dt>${htmlEscapeAttribute(label)}</dt>
8575
+ <dd>${value}</dd>
8576
+ </div>`;
8577
+ }).join("");
8578
+ return `<section class="verification-block" data-solution-block="verification">
8579
+ <div class="verification-block__header">
8580
+ <span class="verification-block__badge">验证方案</span>
8581
+ <span>执行与通过标准</span>
8582
+ </div>
8583
+ <dl class="verification-block__fields">${rows}</dl>
8584
+ </section>`;
8585
+ }
8586
+
8587
+ function prdWorkflowReviewRenderIncompleteVerification(rawLines = []) {
8588
+ const body = prdWorkflowReviewMarkdownLinesToHtml(rawLines);
8589
+ return `<section class="verification-block is-incomplete" data-solution-block="verification">
8590
+ <div class="verification-block__header">
8591
+ <span class="verification-block__badge">验证方案</span>
8592
+ <strong>验证块格式不完整</strong>
8593
+ </div>
8594
+ ${body ? `<div class="verification-block__fallback">${body}</div>` : ""}
8595
+ </section>`;
8596
+ }
8597
+
8375
8598
  function prdWorkflowReviewParseChangeIntent(lines, startIndex) {
8376
8599
  const start = String(lines[startIndex] || "")
8377
8600
  .trim()
@@ -8654,6 +8877,17 @@ function prdWorkflowReviewMarkdownLinesToHtml(lines) {
8654
8877
  flushBlocks();
8655
8878
  continue;
8656
8879
  }
8880
+ if (/^#verification\s*$/i.test(trimmed)) {
8881
+ const parsed = prdWorkflowReviewParseVerificationBlock(lines, i);
8882
+ if (parsed) {
8883
+ flushBlocks();
8884
+ html.push(parsed.complete
8885
+ ? prdWorkflowReviewRenderVerificationBlock(parsed.verification)
8886
+ : prdWorkflowReviewRenderIncompleteVerification(parsed.rawLines));
8887
+ i = parsed.endIndex;
8888
+ continue;
8889
+ }
8890
+ }
8657
8891
  if (/^#change\s+/i.test(trimmed)) {
8658
8892
  const parsed = prdWorkflowReviewParseChangeIntent(lines, i);
8659
8893
  if (parsed) {
@@ -9109,6 +9343,24 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
9109
9343
  .change-intent__proposal-line.is-code { display: grid; grid-template-columns: 2rem minmax(max-content, 1fr); margin: 0 -14px; padding: 0 14px; background: var(--change-code); }
9110
9344
  .change-intent__proposal-mark { color: var(--change-add); font-weight: 900; text-align: center; user-select: none; }
9111
9345
  .change-intent__proposal-text { white-space: pre; }
9346
+ .verification-block { max-width: 100%; margin: 1rem 0 1.2rem; overflow: hidden; border: 1px solid var(--change-border); border-radius: 12px; background: var(--panel-strong); }
9347
+ .verification-block__header { display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--change-border); background: var(--change-header); padding: 11px 14px; color: var(--heading); font-size: 13px; font-weight: 900; }
9348
+ .verification-block__badge { flex: 0 0 auto; border: 1px solid currentColor; border-radius: 999px; color: var(--change-move); padding: 4px 9px; font-size: 12px; line-height: 1.3; }
9349
+ .verification-block__fields { margin: 0; }
9350
+ .verification-block__field { display: grid; grid-template-columns: minmax(7rem, 9rem) minmax(0, 1fr); border-top: 1px solid var(--border-soft); }
9351
+ .verification-block__field:first-child { border-top: 0; }
9352
+ .verification-block__field dt { background: var(--change-context); color: var(--body); padding: 11px 14px; font-size: 13px; font-weight: 900; }
9353
+ .verification-block__field dd { min-width: 0; margin: 0; padding: 10px 14px 12px; color: var(--body); }
9354
+ .verification-block__field dd > *:first-child { margin-top: 0; }
9355
+ .verification-block__field dd > *:last-child { margin-bottom: 0; }
9356
+ .verification-block__field.is-missing { opacity: .72; }
9357
+ .verification-block__missing { color: var(--muted); font-size: 13px; font-style: italic; }
9358
+ .verification-block.is-incomplete { border-color: var(--pending-border); }
9359
+ .verification-block.is-incomplete .verification-block__header { border-bottom-color: var(--pending-border); background: var(--pending-bg); color: var(--pending-text); }
9360
+ .verification-block.is-incomplete .verification-block__badge { color: var(--pending-text); }
9361
+ .verification-block__fallback { padding: 10px 14px 12px; }
9362
+ .verification-block__fallback > *:first-child { margin-top: 0; }
9363
+ .verification-block__fallback > *:last-child { margin-bottom: 0; }
9112
9364
  .syntax-comment { color: var(--syntax-comment); font-style: italic; }
9113
9365
  .syntax-keyword { color: var(--syntax-keyword); font-weight: 700; }
9114
9366
  .syntax-literal { color: var(--syntax-literal); font-weight: 650; }
@@ -9154,6 +9406,8 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
9154
9406
  main { padding: 28px 14px 48px; }
9155
9407
  .change-intent__header { align-items: flex-start; flex-direction: column; }
9156
9408
  .change-intent__locator { width: 100%; }
9409
+ .verification-block__field { grid-template-columns: 1fr; }
9410
+ .verification-block__field dt { border-bottom: 1px solid var(--border-soft); padding-bottom: 8px; }
9157
9411
  .change-intent__source-line, .planned-code__line { grid-template-columns: 3.25rem minmax(max-content, 1fr); }
9158
9412
  header { display: block; }
9159
9413
  .toolbar { justify-content: flex-start; margin-top: 14px; }
@@ -12086,6 +12340,27 @@ export function startUiServer({
12086
12340
  json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
12087
12341
  return;
12088
12342
  }
12343
+ if (req.method === "GET" && url.pathname === "/api/prd-workflows") {
12344
+ if (!authUser?.userId) {
12345
+ json(res, 401, { error: "Unauthorized" });
12346
+ return;
12347
+ }
12348
+ try {
12349
+ const workflows = listPrdWorkflowCollaborationsForUser(userCtx.userId).map((record) => {
12350
+ const stateRoot = path.resolve(getAgentflowUserDataRoot(record.ownerId));
12351
+ const tapdId = String(record.tapdId || "").trim();
12352
+ const project = prdWorkflowReadProjectState(stateRoot, tapdId);
12353
+ const latestClient = prdWorkflowLatestClientSnapshot(stateRoot, stateRoot, tapdId);
12354
+ const legacy = prdWorkflowReadCachedSnapshot(stateRoot, tapdId);
12355
+ const snapshot = project?.snapshot || latestClient || legacy?.snapshot || {};
12356
+ return prdWorkflowDashboardSummary(record, snapshot, userCtx);
12357
+ });
12358
+ json(res, 200, { ok: true, workflows });
12359
+ } catch (error) {
12360
+ json(res, 500, { error: (error && error.message) || String(error) });
12361
+ }
12362
+ return;
12363
+ }
12089
12364
  if (req.method === "GET" && url.pathname === "/api/prd-workflow/share") {
12090
12365
  const tapdId = String(url.searchParams.get("tapdId") || "").trim();
12091
12366
  const shareToken = String(url.searchParams.get("workflowShare") || "").trim();