@zzclub/pipeline 0.4.0 → 0.9.0
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.
- package/README.md +102 -3
- package/package.json +1 -1
- package/src/adapter-types.ts +6 -0
- package/src/adapters/builtin-image-renderer.ts +7 -1
- package/src/adapters/builtin-markdown-renderer.ts +1 -0
- package/src/commands/analytics.test.ts +155 -0
- package/src/commands/analytics.ts +223 -0
- package/src/commands/init.ts +42 -3
- package/src/commands/publish.ts +44 -67
- package/src/commands/republish.ts +146 -0
- package/src/commands/sync-blog.ts +19 -3
- package/src/commands/topic.test.ts +146 -0
- package/src/commands/topic.ts +717 -0
- package/src/commands/wechat-export.ts +2 -0
- package/src/commands/wx-draft-delete.ts +36 -0
- package/src/commands/wx-drafts.ts +47 -0
- package/src/config.ts +0 -0
- package/src/db.test.ts +47 -0
- package/src/db.ts +95 -0
- package/src/imgx/assets/icons/ancientone-logo.png +0 -0
- package/src/imgx/assets/templates/wechat-cover-split.html +1 -1
- package/src/imgx/render-card.ts +1 -5
- package/src/plugins.ts +11 -1
- package/src/providers/blog.ts +9 -3
- package/src/providers/index.test.ts +1 -1
- package/src/providers/index.ts +29 -5
- package/src/providers/publish-core.ts +124 -0
- package/src/providers/wechat.ts +260 -28
- package/src/routes.ts +1 -1
- package/src/schema/analytics.test.ts +54 -0
- package/src/schema/analytics.ts +19 -0
- package/src/schema/config.ts +25 -1
- package/src/schema/state.ts +10 -0
- package/src/schema/topic.test.ts +69 -0
- package/src/schema/topic.ts +55 -0
- package/src/state.ts +1 -0
- package/src/text.test.ts +11 -6
- package/src/text.ts +27 -9
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +2 -7
- package/src/wechat-preview/assets/browser-dist/assets/{style-BYNXnUvc.css → style-vhHYSP8T.css} +1 -1
- package/src/wechat-preview/assets/browser-dist/editor-export.js +27 -27
- package/src/wechat-preview/browser/editor-export.css +248 -159
- package/src/wechat-preview/index.ts +28 -11
- package/src/wechat-preview/themes.ts +70 -24
- package/src/wechat-preview/wechat-formatter.test.ts +0 -1
- package/src/wechat-preview/wechat-formatter.ts +48 -23
- package/src/workflow.test.ts +294 -1
- package/src/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { ensureDb } from "../db";
|
|
2
|
+
import {
|
|
3
|
+
TopicSchema,
|
|
4
|
+
type Topic,
|
|
5
|
+
type TopicStatus,
|
|
6
|
+
type TopicPriority,
|
|
7
|
+
} from "../schema/topic";
|
|
8
|
+
import { parseArgs, requireArg, optionalArg } from "../args";
|
|
9
|
+
import { printResult } from "../output";
|
|
10
|
+
|
|
11
|
+
function generateTopicId(): string {
|
|
12
|
+
const timestamp = Date.now();
|
|
13
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
14
|
+
return `topic_${timestamp}_${random}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rowToTopic(row: Record<string, unknown>): Topic {
|
|
18
|
+
return TopicSchema.parse({
|
|
19
|
+
...row,
|
|
20
|
+
tags: row.tags ? JSON.parse(row.tags as string) : [],
|
|
21
|
+
retro_metrics_snapshot: row.retro_metrics_snapshot
|
|
22
|
+
? JSON.parse(row.retro_metrics_snapshot as string)
|
|
23
|
+
: null,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AddTopicInput {
|
|
28
|
+
title: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
priority?: TopicPriority;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
notes?: string;
|
|
33
|
+
status?: TopicStatus;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function addTopic(
|
|
37
|
+
workspace: string,
|
|
38
|
+
input: AddTopicInput,
|
|
39
|
+
): Promise<Topic> {
|
|
40
|
+
const db = ensureDb(workspace);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
const topicId = generateTopicId();
|
|
45
|
+
|
|
46
|
+
db.prepare(
|
|
47
|
+
`INSERT INTO topics (
|
|
48
|
+
topic_id, title, description, priority, tags, notes, status,
|
|
49
|
+
created_at, updated_at
|
|
50
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
51
|
+
).run(
|
|
52
|
+
topicId,
|
|
53
|
+
input.title,
|
|
54
|
+
input.description ?? null,
|
|
55
|
+
input.priority ?? "medium",
|
|
56
|
+
JSON.stringify(input.tags ?? []),
|
|
57
|
+
input.notes ?? null,
|
|
58
|
+
input.status ?? "backlog",
|
|
59
|
+
now,
|
|
60
|
+
now,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const row = db
|
|
64
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
65
|
+
.get(topicId) as Record<string, unknown>;
|
|
66
|
+
return rowToTopic(row);
|
|
67
|
+
} finally {
|
|
68
|
+
db.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ListTopicsFilter {
|
|
73
|
+
status?: TopicStatus;
|
|
74
|
+
priority?: TopicPriority;
|
|
75
|
+
tag?: string;
|
|
76
|
+
limit?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface UpdateTopicInput {
|
|
80
|
+
status?: TopicStatus;
|
|
81
|
+
priority?: TopicPriority;
|
|
82
|
+
ai_score?: number;
|
|
83
|
+
ai_reason?: string;
|
|
84
|
+
tags?: string[];
|
|
85
|
+
notes?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function updateTopic(
|
|
89
|
+
workspace: string,
|
|
90
|
+
topicId: string,
|
|
91
|
+
input: UpdateTopicInput,
|
|
92
|
+
): Promise<Topic> {
|
|
93
|
+
const db = ensureDb(workspace);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const existing = db
|
|
97
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
98
|
+
.get(topicId);
|
|
99
|
+
if (!existing) {
|
|
100
|
+
throw new Error(`Topic not found: ${topicId}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const updates: string[] = [];
|
|
104
|
+
const params: (string | number | null)[] = [];
|
|
105
|
+
|
|
106
|
+
if (input.status !== undefined) {
|
|
107
|
+
updates.push("status = ?");
|
|
108
|
+
params.push(input.status);
|
|
109
|
+
}
|
|
110
|
+
if (input.priority !== undefined) {
|
|
111
|
+
updates.push("priority = ?");
|
|
112
|
+
params.push(input.priority);
|
|
113
|
+
}
|
|
114
|
+
if (input.ai_score !== undefined) {
|
|
115
|
+
updates.push("ai_score = ?");
|
|
116
|
+
params.push(input.ai_score);
|
|
117
|
+
}
|
|
118
|
+
if (input.ai_reason !== undefined) {
|
|
119
|
+
updates.push("ai_reason = ?");
|
|
120
|
+
params.push(input.ai_reason);
|
|
121
|
+
}
|
|
122
|
+
if (input.tags !== undefined) {
|
|
123
|
+
updates.push("tags = ?");
|
|
124
|
+
params.push(JSON.stringify(input.tags));
|
|
125
|
+
}
|
|
126
|
+
if (input.notes !== undefined) {
|
|
127
|
+
updates.push("notes = ?");
|
|
128
|
+
params.push(input.notes);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
updates.push("updated_at = ?");
|
|
132
|
+
params.push(new Date().toISOString());
|
|
133
|
+
params.push(topicId);
|
|
134
|
+
|
|
135
|
+
db
|
|
136
|
+
.prepare(`UPDATE topics SET ${updates.join(", ")} WHERE topic_id = ?`)
|
|
137
|
+
.run(...params);
|
|
138
|
+
|
|
139
|
+
const row = db
|
|
140
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
141
|
+
.get(topicId) as Record<string, unknown>;
|
|
142
|
+
return rowToTopic(row);
|
|
143
|
+
} finally {
|
|
144
|
+
db.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ScheduleTopicInput {
|
|
149
|
+
scheduled_date: string;
|
|
150
|
+
target_account?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function scheduleTopic(
|
|
154
|
+
workspace: string,
|
|
155
|
+
topicId: string,
|
|
156
|
+
input: ScheduleTopicInput,
|
|
157
|
+
): Promise<Topic> {
|
|
158
|
+
const db = ensureDb(workspace);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const existing = db
|
|
162
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
163
|
+
.get(topicId);
|
|
164
|
+
if (!existing) {
|
|
165
|
+
throw new Error(`Topic not found: ${topicId}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const now = new Date().toISOString();
|
|
169
|
+
db.prepare(
|
|
170
|
+
`UPDATE topics
|
|
171
|
+
SET status = 'scheduled',
|
|
172
|
+
scheduled_date = ?,
|
|
173
|
+
target_account = ?,
|
|
174
|
+
updated_at = ?
|
|
175
|
+
WHERE topic_id = ?`,
|
|
176
|
+
).run(input.scheduled_date, input.target_account ?? null, now, topicId);
|
|
177
|
+
|
|
178
|
+
const row = db
|
|
179
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
180
|
+
.get(topicId) as Record<string, unknown>;
|
|
181
|
+
return rowToTopic(row);
|
|
182
|
+
} finally {
|
|
183
|
+
db.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface RetroTopicInput {
|
|
188
|
+
performance: "excellent" | "good" | "average" | "poor";
|
|
189
|
+
lessons?: string;
|
|
190
|
+
metrics_snapshot?: Record<string, unknown>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function retroTopic(
|
|
194
|
+
workspace: string,
|
|
195
|
+
topicId: string,
|
|
196
|
+
input: RetroTopicInput,
|
|
197
|
+
): Promise<Topic> {
|
|
198
|
+
const db = ensureDb(workspace);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const existing = db
|
|
202
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
203
|
+
.get(topicId);
|
|
204
|
+
if (!existing) {
|
|
205
|
+
throw new Error(`Topic not found: ${topicId}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const now = new Date().toISOString();
|
|
209
|
+
db.prepare(
|
|
210
|
+
`UPDATE topics
|
|
211
|
+
SET status = 'published',
|
|
212
|
+
retro_performance = ?,
|
|
213
|
+
retro_lessons = ?,
|
|
214
|
+
retro_metrics_snapshot = ?,
|
|
215
|
+
updated_at = ?
|
|
216
|
+
WHERE topic_id = ?`,
|
|
217
|
+
).run(
|
|
218
|
+
input.performance,
|
|
219
|
+
input.lessons ?? null,
|
|
220
|
+
input.metrics_snapshot ? JSON.stringify(input.metrics_snapshot) : null,
|
|
221
|
+
now,
|
|
222
|
+
topicId,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const row = db
|
|
226
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
227
|
+
.get(topicId) as Record<string, unknown>;
|
|
228
|
+
return rowToTopic(row);
|
|
229
|
+
} finally {
|
|
230
|
+
db.close();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface AbandonTopicInput {
|
|
235
|
+
reason?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function abandonTopic(
|
|
239
|
+
workspace: string,
|
|
240
|
+
topicId: string,
|
|
241
|
+
input: AbandonTopicInput,
|
|
242
|
+
): Promise<Topic> {
|
|
243
|
+
const db = ensureDb(workspace);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const existing = db
|
|
247
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
248
|
+
.get(topicId) as Record<string, unknown> | undefined;
|
|
249
|
+
if (!existing) {
|
|
250
|
+
throw new Error(`Topic not found: ${topicId}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const existingNotes = (existing.notes as string) ?? "";
|
|
254
|
+
const newNotes = input.reason
|
|
255
|
+
? `${existingNotes}\n[Abandoned] ${input.reason}`.trim()
|
|
256
|
+
: existingNotes;
|
|
257
|
+
|
|
258
|
+
const now = new Date().toISOString();
|
|
259
|
+
db.prepare(
|
|
260
|
+
`UPDATE topics
|
|
261
|
+
SET status = 'abandoned',
|
|
262
|
+
notes = ?,
|
|
263
|
+
updated_at = ?
|
|
264
|
+
WHERE topic_id = ?`,
|
|
265
|
+
).run(newNotes, now, topicId);
|
|
266
|
+
|
|
267
|
+
const row = db
|
|
268
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
269
|
+
.get(topicId) as Record<string, unknown>;
|
|
270
|
+
return rowToTopic(row);
|
|
271
|
+
} finally {
|
|
272
|
+
db.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface EvaluateTopicInput {
|
|
277
|
+
ai_score: number;
|
|
278
|
+
ai_reason?: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function evaluateTopic(
|
|
282
|
+
workspace: string,
|
|
283
|
+
topicId: string,
|
|
284
|
+
input: EvaluateTopicInput,
|
|
285
|
+
): Promise<Topic> {
|
|
286
|
+
const db = ensureDb(workspace);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const existing = db
|
|
290
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
291
|
+
.get(topicId);
|
|
292
|
+
if (!existing) {
|
|
293
|
+
throw new Error(`Topic not found: ${topicId}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
db.prepare(
|
|
298
|
+
`UPDATE topics
|
|
299
|
+
SET status = 'evaluating',
|
|
300
|
+
ai_score = ?,
|
|
301
|
+
ai_reason = ?,
|
|
302
|
+
updated_at = ?
|
|
303
|
+
WHERE topic_id = ?`,
|
|
304
|
+
).run(input.ai_score, input.ai_reason ?? null, now, topicId);
|
|
305
|
+
|
|
306
|
+
const row = db
|
|
307
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
308
|
+
.get(topicId) as Record<string, unknown>;
|
|
309
|
+
return rowToTopic(row);
|
|
310
|
+
} finally {
|
|
311
|
+
db.close();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function listTopics(
|
|
316
|
+
workspace: string,
|
|
317
|
+
filter: ListTopicsFilter,
|
|
318
|
+
): Promise<Topic[]> {
|
|
319
|
+
const db = ensureDb(workspace);
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const conditions: string[] = [];
|
|
323
|
+
const params: (string | number)[] = [];
|
|
324
|
+
|
|
325
|
+
if (filter.status) {
|
|
326
|
+
conditions.push("status = ?");
|
|
327
|
+
params.push(filter.status);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (filter.priority) {
|
|
331
|
+
conditions.push("priority = ?");
|
|
332
|
+
params.push(filter.priority);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (filter.tag) {
|
|
336
|
+
conditions.push(
|
|
337
|
+
"EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)",
|
|
338
|
+
);
|
|
339
|
+
params.push(filter.tag);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const where =
|
|
343
|
+
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
344
|
+
const limit = filter.limit ?? 50;
|
|
345
|
+
|
|
346
|
+
const sql = `SELECT * FROM topics ${where} ORDER BY created_at DESC LIMIT ?`;
|
|
347
|
+
params.push(limit);
|
|
348
|
+
|
|
349
|
+
const rows = db.prepare(sql).all(...params) as Record<string, unknown>[];
|
|
350
|
+
return rows.map(rowToTopic);
|
|
351
|
+
} finally {
|
|
352
|
+
db.close();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function topic(args: string[]): Promise<void> {
|
|
357
|
+
const parsed = parseArgs(args);
|
|
358
|
+
|
|
359
|
+
if (parsed.help || args.length === 0) {
|
|
360
|
+
console.log(`
|
|
361
|
+
Usage: zzhub-pipeline topic <subcommand> [options]
|
|
362
|
+
|
|
363
|
+
Subcommands:
|
|
364
|
+
add Add a new topic
|
|
365
|
+
list List topics with filters
|
|
366
|
+
update Update topic fields
|
|
367
|
+
evaluate Evaluate topic with AI score
|
|
368
|
+
schedule Schedule topic for publishing
|
|
369
|
+
promote Promote topic to publish task
|
|
370
|
+
retro Add retrospective to topic
|
|
371
|
+
abandon Abandon a topic
|
|
372
|
+
|
|
373
|
+
Run 'zzhub-pipeline topic <subcommand> --help' for details.
|
|
374
|
+
`.trim());
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const subcommand = args[0];
|
|
379
|
+
const subArgs = args.slice(1);
|
|
380
|
+
|
|
381
|
+
switch (subcommand) {
|
|
382
|
+
case "add":
|
|
383
|
+
return topicAdd(subArgs);
|
|
384
|
+
case "list":
|
|
385
|
+
return topicList(subArgs);
|
|
386
|
+
case "update":
|
|
387
|
+
return topicUpdate(subArgs);
|
|
388
|
+
case "evaluate":
|
|
389
|
+
return topicEvaluate(subArgs);
|
|
390
|
+
case "schedule":
|
|
391
|
+
return topicSchedule(subArgs);
|
|
392
|
+
case "promote":
|
|
393
|
+
return topicPromote(subArgs);
|
|
394
|
+
case "retro":
|
|
395
|
+
return topicRetro(subArgs);
|
|
396
|
+
case "abandon":
|
|
397
|
+
return topicAbandon(subArgs);
|
|
398
|
+
default:
|
|
399
|
+
throw new Error(`Unknown topic subcommand: ${subcommand}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function topicAdd(args: string[]): Promise<void> {
|
|
404
|
+
const parsed = parseArgs(args);
|
|
405
|
+
|
|
406
|
+
if (parsed.help) {
|
|
407
|
+
console.log(`
|
|
408
|
+
Usage: zzhub-pipeline topic add [options]
|
|
409
|
+
|
|
410
|
+
Options:
|
|
411
|
+
--workspace Workspace path (required)
|
|
412
|
+
--title Topic title (required)
|
|
413
|
+
--description Topic description
|
|
414
|
+
--priority Priority: high/medium/low (default: medium)
|
|
415
|
+
--tags Comma-separated tags
|
|
416
|
+
--notes Notes
|
|
417
|
+
`.trim());
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
422
|
+
const title = requireArg(parsed, "title", "Topic title");
|
|
423
|
+
const description = optionalArg(parsed, "description");
|
|
424
|
+
const priority = optionalArg(parsed, "priority") as TopicPriority | undefined;
|
|
425
|
+
const tagsRaw = optionalArg(parsed, "tags");
|
|
426
|
+
const notes = optionalArg(parsed, "notes");
|
|
427
|
+
|
|
428
|
+
const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()) : undefined;
|
|
429
|
+
|
|
430
|
+
const result = await addTopic(workspace, {
|
|
431
|
+
title,
|
|
432
|
+
description,
|
|
433
|
+
priority,
|
|
434
|
+
tags,
|
|
435
|
+
notes,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
printResult(result);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function topicList(args: string[]): Promise<void> {
|
|
442
|
+
const parsed = parseArgs(args);
|
|
443
|
+
|
|
444
|
+
if (parsed.help) {
|
|
445
|
+
console.log(`
|
|
446
|
+
Usage: zzhub-pipeline topic list [options]
|
|
447
|
+
|
|
448
|
+
Options:
|
|
449
|
+
--workspace Workspace path (required)
|
|
450
|
+
--status Filter by status
|
|
451
|
+
--priority Filter by priority
|
|
452
|
+
--tag Filter by tag
|
|
453
|
+
--sort Sort field: priority/created_at/scheduled_date/ai_score
|
|
454
|
+
--limit Max results (default: 50)
|
|
455
|
+
--view Output format: json/markdown/agent (default: json)
|
|
456
|
+
`.trim());
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
461
|
+
const status = optionalArg(parsed, "status") as TopicStatus | undefined;
|
|
462
|
+
const priority = optionalArg(parsed, "priority") as TopicPriority | undefined;
|
|
463
|
+
const tag = optionalArg(parsed, "tag");
|
|
464
|
+
const limitStr = optionalArg(parsed, "limit");
|
|
465
|
+
const limit = limitStr ? parseInt(limitStr, 10) : undefined;
|
|
466
|
+
|
|
467
|
+
const topics = await listTopics(workspace, { status, priority, tag, limit });
|
|
468
|
+
|
|
469
|
+
printResult(topics);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function topicUpdate(args: string[]): Promise<void> {
|
|
473
|
+
const parsed = parseArgs(args);
|
|
474
|
+
|
|
475
|
+
if (parsed.help) {
|
|
476
|
+
console.log(`
|
|
477
|
+
Usage: zzhub-pipeline topic update [options]
|
|
478
|
+
|
|
479
|
+
Options:
|
|
480
|
+
--workspace Workspace path (required)
|
|
481
|
+
--topic Topic ID (required)
|
|
482
|
+
--status New status
|
|
483
|
+
--priority New priority
|
|
484
|
+
--ai-score AI score (0-100)
|
|
485
|
+
--ai-reason AI evaluation reason
|
|
486
|
+
--tags Comma-separated tags
|
|
487
|
+
--notes Notes
|
|
488
|
+
`.trim());
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
493
|
+
const topicId = requireArg(parsed, "topic", "Topic ID");
|
|
494
|
+
const status = optionalArg(parsed, "status") as TopicStatus | undefined;
|
|
495
|
+
const priority = optionalArg(parsed, "priority") as TopicPriority | undefined;
|
|
496
|
+
const aiScoreStr = optionalArg(parsed, "ai-score");
|
|
497
|
+
const aiScore = aiScoreStr ? parseInt(aiScoreStr, 10) : undefined;
|
|
498
|
+
const aiReason = optionalArg(parsed, "ai-reason");
|
|
499
|
+
const tagsRaw = optionalArg(parsed, "tags");
|
|
500
|
+
const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()) : undefined;
|
|
501
|
+
const notes = optionalArg(parsed, "notes");
|
|
502
|
+
|
|
503
|
+
const result = await updateTopic(workspace, topicId, {
|
|
504
|
+
status,
|
|
505
|
+
priority,
|
|
506
|
+
ai_score: aiScore,
|
|
507
|
+
ai_reason: aiReason,
|
|
508
|
+
tags,
|
|
509
|
+
notes,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
printResult(result);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function topicEvaluate(args: string[]): Promise<void> {
|
|
516
|
+
const parsed = parseArgs(args);
|
|
517
|
+
|
|
518
|
+
if (parsed.help) {
|
|
519
|
+
console.log(`
|
|
520
|
+
Usage: zzhub-pipeline topic evaluate [options]
|
|
521
|
+
|
|
522
|
+
Options:
|
|
523
|
+
--workspace Workspace path (required)
|
|
524
|
+
--topic Topic ID (required)
|
|
525
|
+
--ai-score AI score 0-100 (required)
|
|
526
|
+
--ai-reason AI evaluation reason
|
|
527
|
+
`.trim());
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
532
|
+
const topicId = requireArg(parsed, "topic", "Topic ID");
|
|
533
|
+
const aiScoreStr = requireArg(parsed, "ai-score", "AI score (0-100)");
|
|
534
|
+
const aiScore = parseInt(aiScoreStr, 10);
|
|
535
|
+
const aiReason = optionalArg(parsed, "ai-reason");
|
|
536
|
+
|
|
537
|
+
const result = await evaluateTopic(workspace, topicId, {
|
|
538
|
+
ai_score: aiScore,
|
|
539
|
+
ai_reason: aiReason,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
printResult(result);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function topicSchedule(args: string[]): Promise<void> {
|
|
546
|
+
const parsed = parseArgs(args);
|
|
547
|
+
|
|
548
|
+
if (parsed.help) {
|
|
549
|
+
console.log(`
|
|
550
|
+
Usage: zzhub-pipeline topic schedule [options]
|
|
551
|
+
|
|
552
|
+
Options:
|
|
553
|
+
--workspace Workspace path (required)
|
|
554
|
+
--topic Topic ID (required)
|
|
555
|
+
--scheduled-date Scheduled date YYYY-MM-DD (required)
|
|
556
|
+
--target-account Target account
|
|
557
|
+
`.trim());
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
562
|
+
const topicId = requireArg(parsed, "topic", "Topic ID");
|
|
563
|
+
const scheduledDate = requireArg(parsed, "scheduled-date", "Scheduled date");
|
|
564
|
+
const targetAccount = optionalArg(parsed, "target-account");
|
|
565
|
+
|
|
566
|
+
const result = await scheduleTopic(workspace, topicId, {
|
|
567
|
+
scheduled_date: scheduledDate,
|
|
568
|
+
target_account: targetAccount,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
printResult(result);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function topicPromote(args: string[]): Promise<void> {
|
|
575
|
+
const parsed = parseArgs(args);
|
|
576
|
+
|
|
577
|
+
if (parsed.help) {
|
|
578
|
+
console.log(`
|
|
579
|
+
Usage: zzhub-pipeline topic promote [options]
|
|
580
|
+
|
|
581
|
+
Promote a topic to a publish task. This will:
|
|
582
|
+
1. Create a new publish task using 'init' command
|
|
583
|
+
2. Update topic status to 'in_progress'
|
|
584
|
+
3. Link the run_id to the topic
|
|
585
|
+
|
|
586
|
+
Options:
|
|
587
|
+
--workspace Workspace path (required)
|
|
588
|
+
--topic Topic ID (required)
|
|
589
|
+
--intent-text Intent text for init (required)
|
|
590
|
+
--content-form article | newspic (default: article)
|
|
591
|
+
--targets Comma-separated: wechat,blog (default: wechat)
|
|
592
|
+
--account Target account
|
|
593
|
+
`.trim());
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
598
|
+
const topicId = requireArg(parsed, "topic", "Topic ID");
|
|
599
|
+
const intentText = requireArg(parsed, "intent-text", "Intent text");
|
|
600
|
+
const contentForm = optionalArg(parsed, "content-form") ?? "article";
|
|
601
|
+
const targets = optionalArg(parsed, "targets") ?? "wechat";
|
|
602
|
+
const account = optionalArg(parsed, "account");
|
|
603
|
+
|
|
604
|
+
// Get topic to verify it exists
|
|
605
|
+
const db = ensureDb(workspace);
|
|
606
|
+
try {
|
|
607
|
+
const topicRow = db
|
|
608
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
609
|
+
.get(topicId) as Record<string, unknown> | undefined;
|
|
610
|
+
if (!topicRow) {
|
|
611
|
+
throw new Error(`Topic not found: ${topicId}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Build init command args
|
|
615
|
+
const initArgs = [
|
|
616
|
+
"--workspace", workspace,
|
|
617
|
+
"--task-kind", "publish",
|
|
618
|
+
"--content-form", contentForm,
|
|
619
|
+
"--targets", targets,
|
|
620
|
+
"--content-origin", "user",
|
|
621
|
+
"--intent-text", intentText,
|
|
622
|
+
];
|
|
623
|
+
if (account) {
|
|
624
|
+
initArgs.push("--account", account);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Call init command
|
|
628
|
+
const { init } = await import("./init");
|
|
629
|
+
await init(initArgs);
|
|
630
|
+
|
|
631
|
+
// Note: We can't easily capture the run_id from init's output
|
|
632
|
+
// For now, just update the topic status
|
|
633
|
+
const now = new Date().toISOString();
|
|
634
|
+
db.prepare(
|
|
635
|
+
`UPDATE topics
|
|
636
|
+
SET status = 'in_progress',
|
|
637
|
+
updated_at = ?
|
|
638
|
+
WHERE topic_id = ?`,
|
|
639
|
+
).run(now, topicId);
|
|
640
|
+
|
|
641
|
+
const updatedRow = db
|
|
642
|
+
.prepare("SELECT * FROM topics WHERE topic_id = ?")
|
|
643
|
+
.get(topicId) as Record<string, unknown>;
|
|
644
|
+
const updatedTopic = rowToTopic(updatedRow);
|
|
645
|
+
|
|
646
|
+
printResult({
|
|
647
|
+
topic: updatedTopic,
|
|
648
|
+
message: "Topic promoted to in_progress. Run 'zzp find-run --workspace {workspace} --active' to find the new task.",
|
|
649
|
+
init_args: initArgs,
|
|
650
|
+
});
|
|
651
|
+
} finally {
|
|
652
|
+
db.close();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function topicRetro(args: string[]): Promise<void> {
|
|
657
|
+
const parsed = parseArgs(args);
|
|
658
|
+
|
|
659
|
+
if (parsed.help) {
|
|
660
|
+
console.log(`
|
|
661
|
+
Usage: zzhub-pipeline topic retro [options]
|
|
662
|
+
|
|
663
|
+
Options:
|
|
664
|
+
--workspace Workspace path (required)
|
|
665
|
+
--topic Topic ID (required)
|
|
666
|
+
--performance Performance: excellent/good/average/poor (required)
|
|
667
|
+
--lessons Lessons learned
|
|
668
|
+
--metrics-snapshot Metrics snapshot as JSON string
|
|
669
|
+
`.trim());
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
674
|
+
const topicId = requireArg(parsed, "topic", "Topic ID");
|
|
675
|
+
const performance = requireArg(parsed, "performance", "Performance rating") as
|
|
676
|
+
| "excellent"
|
|
677
|
+
| "good"
|
|
678
|
+
| "average"
|
|
679
|
+
| "poor";
|
|
680
|
+
const lessons = optionalArg(parsed, "lessons");
|
|
681
|
+
const metricsSnapshotRaw = optionalArg(parsed, "metrics-snapshot");
|
|
682
|
+
const metricsSnapshot = metricsSnapshotRaw
|
|
683
|
+
? JSON.parse(metricsSnapshotRaw)
|
|
684
|
+
: undefined;
|
|
685
|
+
|
|
686
|
+
const result = await retroTopic(workspace, topicId, {
|
|
687
|
+
performance,
|
|
688
|
+
lessons,
|
|
689
|
+
metrics_snapshot: metricsSnapshot,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
printResult(result);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function topicAbandon(args: string[]): Promise<void> {
|
|
696
|
+
const parsed = parseArgs(args);
|
|
697
|
+
|
|
698
|
+
if (parsed.help) {
|
|
699
|
+
console.log(`
|
|
700
|
+
Usage: zzhub-pipeline topic abandon [options]
|
|
701
|
+
|
|
702
|
+
Options:
|
|
703
|
+
--workspace Workspace path (required)
|
|
704
|
+
--topic Topic ID (required)
|
|
705
|
+
--reason Reason for abandonment
|
|
706
|
+
`.trim());
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const workspace = requireArg(parsed, "workspace", "Workspace path");
|
|
711
|
+
const topicId = requireArg(parsed, "topic", "Topic ID");
|
|
712
|
+
const reason = optionalArg(parsed, "reason");
|
|
713
|
+
|
|
714
|
+
const result = await abandonTopic(workspace, topicId, { reason });
|
|
715
|
+
|
|
716
|
+
printResult(result);
|
|
717
|
+
}
|