shennian 0.2.76 → 0.2.78
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/dist/scripts/wechat-rpa-download-candidates.mjs +21 -0
- package/dist/scripts/wechat-rpa-win-visual.mjs +73 -34
- package/dist/scripts/wechat-rpa-win.mjs +10 -3
- package/dist/src/channels/wechat-rpa/normalizer.d.ts +3 -0
- package/dist/src/channels/wechat-rpa/normalizer.js +16 -1
- package/dist/src/channels/wechat-rpa.js +37 -0
- package/dist/src/upgrade/engine.d.ts +1 -0
- package/dist/src/upgrade/engine.js +30 -2
- package/package.json +8 -9
|
@@ -24,6 +24,27 @@ export function selectDownloadedAttachment(before, after, startedAt, attachment)
|
|
|
24
24
|
.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs)[0] || null
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function selectCachedAttachment(candidates, attachment) {
|
|
28
|
+
const expectedName = normalizeReplyText(attachment?.name || '')
|
|
29
|
+
if (expectedName.length < 4) return null
|
|
30
|
+
const expectedExt = path.extname(String(attachment?.name || '')).toLowerCase()
|
|
31
|
+
const expectedHead = expectedName.slice(0, Math.min(expectedName.length, 24))
|
|
32
|
+
const matched = Array.from(candidates.values())
|
|
33
|
+
.filter((file) => isPlausibleDownloadedAttachment(file, attachment))
|
|
34
|
+
.filter((file) => {
|
|
35
|
+
const base = normalizeReplyText(path.basename(file.path))
|
|
36
|
+
if (base === expectedName) return true
|
|
37
|
+
return expectedHead.length >= 8 && base.includes(expectedHead)
|
|
38
|
+
})
|
|
39
|
+
if (!matched.length) return null
|
|
40
|
+
return matched
|
|
41
|
+
.map((file) => ({
|
|
42
|
+
...file,
|
|
43
|
+
score: (path.extname(file.path).toLowerCase() === expectedExt ? 10 : 0) + file.mtimeMs / 1_000_000_000_000,
|
|
44
|
+
}))
|
|
45
|
+
.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs)[0] || null
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
const INTERNAL_EXTENSIONS = new Set([
|
|
28
49
|
'.db',
|
|
29
50
|
'.ini',
|
|
@@ -151,6 +151,11 @@ export function findTitleConfirmation(observations, targetName) {
|
|
|
151
151
|
return rows.find(row => normalizeConversationName(observationText(row)) === target) || null
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
export function isRetryableOcrError(status, body) {
|
|
155
|
+
if ([408, 429, 500, 502, 503, 504].includes(Number(status))) return true
|
|
156
|
+
return /invalid model response|timeout|temporar/i.test(String(body || ''))
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
export function pointFromObservation(capturePayload, observation, imageSize) {
|
|
155
160
|
if (!capturePayload?.bounds) throw new Error('capture payload missing bounds')
|
|
156
161
|
if (!observation?.box) throw new Error('observation missing box')
|
|
@@ -286,22 +291,33 @@ async function recognizeScreenshot(capture, options, purpose) {
|
|
|
286
291
|
const token = options.token || process.env.WECHAT_RPA_OCR_TOKEN || ''
|
|
287
292
|
if (!token) throw new Error('Missing OCR bearer token. Pass --token or WECHAT_RPA_OCR_TOKEN.')
|
|
288
293
|
const imageBase64 = fs.readFileSync(capture.file).toString('base64')
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
294
|
+
const maxAttempts = Math.max(1, Number(options.ocrRetries || 2) + 1)
|
|
295
|
+
let lastError = null
|
|
296
|
+
let response = null
|
|
297
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
298
|
+
response = await fetch(options.ocrUrl, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: {
|
|
301
|
+
'Content-Type': 'application/json',
|
|
302
|
+
Authorization: `Bearer ${token}`,
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify({
|
|
305
|
+
imageBase64,
|
|
306
|
+
mimeType: 'image/png',
|
|
307
|
+
purpose,
|
|
308
|
+
conversationName: options.group,
|
|
309
|
+
channelId: options.channelId,
|
|
310
|
+
}),
|
|
311
|
+
signal: AbortSignal.timeout(options.ocrTimeoutMs),
|
|
312
|
+
})
|
|
313
|
+
if (response.ok) break
|
|
314
|
+
const body = await response.text()
|
|
315
|
+
lastError = `OCR request failed: ${response.status} ${body}`
|
|
316
|
+
if (attempt >= maxAttempts || !isRetryableOcrError(response.status, body)) {
|
|
317
|
+
throw new Error(lastError)
|
|
318
|
+
}
|
|
319
|
+
await sleep(400 * attempt)
|
|
320
|
+
}
|
|
305
321
|
const json = await response.json()
|
|
306
322
|
const debugPath = capture.file.replace(/\.png$/i, `-${purpose}.ocr.json`)
|
|
307
323
|
fs.writeFileSync(debugPath, `${JSON.stringify(json, null, 2)}\n`)
|
|
@@ -380,6 +396,13 @@ function attachmentTypeFromExt(ext) {
|
|
|
380
396
|
return 'file'
|
|
381
397
|
}
|
|
382
398
|
|
|
399
|
+
function postPasteDelayMs(file) {
|
|
400
|
+
const type = classifyOutboundFile(file).type
|
|
401
|
+
if (type === 'image') return 8_000
|
|
402
|
+
if (type === 'video') return 15_000
|
|
403
|
+
return 2_000
|
|
404
|
+
}
|
|
405
|
+
|
|
383
406
|
const IMAGE_EXTENSIONS = new Set(['.apng', '.avif', '.bmp', '.gif', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.tif', '.tiff', '.webp'])
|
|
384
407
|
const VIDEO_EXTENSIONS = new Set(['.3g2', '.3gp', '.avi', '.m4v', '.mkv', '.mov', '.mp4', '.mpeg', '.mpg', '.webm', '.wmv'])
|
|
385
408
|
|
|
@@ -523,6 +546,14 @@ export function missingConfirmedFiles(observations, files) {
|
|
|
523
546
|
async function openConversationBySearch(options, artifacts) {
|
|
524
547
|
const initial = await capture('window', options, 'window-before-search')
|
|
525
548
|
artifacts.push(initial.file)
|
|
549
|
+
try {
|
|
550
|
+
const initialTitleOcr = await recognizeScreenshot(initial, options, 'title-confirmation')
|
|
551
|
+
if (findTitleConfirmation(initialTitleOcr.observations, options.group)) {
|
|
552
|
+
return initial
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
process.stderr.write(`Initial title OCR was skipped: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
556
|
+
}
|
|
526
557
|
|
|
527
558
|
await click(geometryPoint(initial.payload, 'search'), options)
|
|
528
559
|
await sleep(180)
|
|
@@ -583,9 +614,9 @@ export async function runVisualFlow(input) {
|
|
|
583
614
|
await click(geometryPoint(opened.payload, 'input'), options)
|
|
584
615
|
await pasteText(options.replyText, options)
|
|
585
616
|
await sleep(250)
|
|
586
|
-
await
|
|
617
|
+
await press('{ENTER}', options)
|
|
587
618
|
await sleep(900)
|
|
588
|
-
const confirmCapture = await capture('
|
|
619
|
+
const confirmCapture = await capture('window', options, 'after-text-send')
|
|
589
620
|
artifacts.push(confirmCapture.file)
|
|
590
621
|
const confirmOcr = await recognizeScreenshot(confirmCapture, options, 'send-confirmation')
|
|
591
622
|
if (!observationsContainText(confirmOcr.observations, options.replyText)) {
|
|
@@ -595,25 +626,33 @@ export async function runVisualFlow(input) {
|
|
|
595
626
|
}
|
|
596
627
|
|
|
597
628
|
if (!options.dryRun && options.files?.length) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
629
|
+
const confirmedFiles = []
|
|
630
|
+
const sentAttachments = []
|
|
631
|
+
const confirmationObservations = []
|
|
632
|
+
for (const [index, file] of options.files.entries()) {
|
|
633
|
+
await click(geometryPoint(opened.payload, 'input'), options)
|
|
634
|
+
await pasteFiles([file], options)
|
|
635
|
+
await sleep(850)
|
|
636
|
+
const pendingCapture = await capture('input', options, `pending-file-${index + 1}`)
|
|
637
|
+
artifacts.push(pendingCapture.file)
|
|
638
|
+
await click(geometryPoint(opened.payload, 'send'), options)
|
|
639
|
+
await sleep(postPasteDelayMs(file))
|
|
640
|
+
const fileConfirmCapture = await capture('window', options, `after-file-${index + 1}-send`)
|
|
641
|
+
artifacts.push(fileConfirmCapture.file)
|
|
642
|
+
const fileConfirmOcr = await recognizeScreenshot(fileConfirmCapture, options, 'send-confirmation')
|
|
643
|
+
const missing = missingConfirmedFiles(fileConfirmOcr.observations, [file])
|
|
644
|
+
if (missing.length > 0) {
|
|
645
|
+
throw new Error(`Sent file was not confirmed by OCR: ${basenameForAnyPlatform(file)}. See ${fileConfirmCapture.file}`)
|
|
646
|
+
}
|
|
647
|
+
confirmedFiles.push(file)
|
|
648
|
+
sentAttachments.push(classifyOutboundFile(file))
|
|
649
|
+
confirmationObservations.push(...summarizeObservations(fileConfirmOcr.observations, options.recentLimit))
|
|
611
650
|
}
|
|
612
651
|
sent.push({
|
|
613
652
|
type: 'files',
|
|
614
|
-
files:
|
|
615
|
-
attachments:
|
|
616
|
-
observations:
|
|
653
|
+
files: confirmedFiles,
|
|
654
|
+
attachments: sentAttachments,
|
|
655
|
+
observations: confirmationObservations.slice(-options.recentLimit),
|
|
617
656
|
})
|
|
618
657
|
}
|
|
619
658
|
|
|
@@ -31,7 +31,6 @@ function printHelp() {
|
|
|
31
31
|
node scripts/wechat-rpa-win.mjs send-text --group <name> --text <message>
|
|
32
32
|
node scripts/wechat-rpa-win.mjs send-files --group <name> --file <path> [--file <path> ...]
|
|
33
33
|
node scripts/wechat-rpa-win.mjs listen --group <name> --seconds 60 --poll-ms 1000
|
|
34
|
-
node scripts/wechat-rpa-win.mjs prepare-uia
|
|
35
34
|
node scripts/wechat-rpa-win.mjs capture --region window --output C:\\tmp\\wechat.png
|
|
36
35
|
node scripts/wechat-rpa-win.mjs click --x 480 --y 470 --no-raise
|
|
37
36
|
node scripts/wechat-rpa-win.mjs paste-text --text "hello"
|
|
@@ -46,8 +45,8 @@ Environment:
|
|
|
46
45
|
|
|
47
46
|
Diagnostic note:
|
|
48
47
|
probe does not raise WeChat by default. Use probe --raise only for diagnostics.
|
|
49
|
-
restart-wechat-for-uia
|
|
50
|
-
|
|
48
|
+
prepare-uia and restart-wechat-for-uia are intentionally hidden from normal help.
|
|
49
|
+
They can start accessibility diagnostics or close WeChat, and must never be used by product flows.`)
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
async function main() {
|
|
@@ -64,6 +63,14 @@ async function main() {
|
|
|
64
63
|
const allowIndex = argv.indexOf('--allow-non-windows')
|
|
65
64
|
if (allowIndex >= 0) argv.splice(allowIndex, 1)
|
|
66
65
|
|
|
66
|
+
const command = argv[0]
|
|
67
|
+
if (command === 'prepare-uia' && process.env.SHENNIAN_WECHAT_RPA_ALLOW_UIA_DIAGNOSTICS !== '1') {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'prepare-uia is diagnostic-only because it can start Windows Narrator. ' +
|
|
70
|
+
'Set SHENNIAN_WECHAT_RPA_ALLOW_UIA_DIAGNOSTICS=1 in a local diagnostic shell to use it.',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
const helper = takeOption(argv, '--helper') ?? process.env.WECHAT_RPA_WIN_HELPER ?? defaultHelper
|
|
68
75
|
if (!fs.existsSync(helper)) {
|
|
69
76
|
throw new Error(
|
|
@@ -36,7 +36,10 @@ export type WeChatRpaNormalizedMessage = {
|
|
|
36
36
|
export declare class WeChatRpaDeduper {
|
|
37
37
|
private seen;
|
|
38
38
|
private queue;
|
|
39
|
+
constructor(initialMessageIds?: string[]);
|
|
39
40
|
accept(messageId: string): boolean;
|
|
41
|
+
snapshot(): string[];
|
|
42
|
+
private trim;
|
|
40
43
|
}
|
|
41
44
|
export declare function normalizeWeChatRpaMessage(input: WeChatRpaObservedMessage): WeChatRpaNormalizedMessage | null;
|
|
42
45
|
export declare function weChatRpaConversationId(conversationName: string): string;
|
|
@@ -5,17 +5,32 @@ const MAX_DEDUP_KEYS = 500;
|
|
|
5
5
|
export class WeChatRpaDeduper {
|
|
6
6
|
seen = new Set();
|
|
7
7
|
queue = [];
|
|
8
|
+
constructor(initialMessageIds = []) {
|
|
9
|
+
for (const id of initialMessageIds) {
|
|
10
|
+
if (!id || this.seen.has(id))
|
|
11
|
+
continue;
|
|
12
|
+
this.seen.add(id);
|
|
13
|
+
this.queue.push(id);
|
|
14
|
+
}
|
|
15
|
+
this.trim();
|
|
16
|
+
}
|
|
8
17
|
accept(messageId) {
|
|
9
18
|
if (this.seen.has(messageId))
|
|
10
19
|
return false;
|
|
11
20
|
this.seen.add(messageId);
|
|
12
21
|
this.queue.push(messageId);
|
|
22
|
+
this.trim();
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
snapshot() {
|
|
26
|
+
return [...this.queue];
|
|
27
|
+
}
|
|
28
|
+
trim() {
|
|
13
29
|
while (this.queue.length > MAX_DEDUP_KEYS) {
|
|
14
30
|
const old = this.queue.shift();
|
|
15
31
|
if (old)
|
|
16
32
|
this.seen.delete(old);
|
|
17
33
|
}
|
|
18
|
-
return true;
|
|
19
34
|
}
|
|
20
35
|
}
|
|
21
36
|
export function normalizeWeChatRpaMessage(input) {
|
|
@@ -30,6 +30,7 @@ export class WeChatRpaChannelAdapter {
|
|
|
30
30
|
conn.stopped = false;
|
|
31
31
|
conn.config = config;
|
|
32
32
|
hydratePendingReplyState(conn, config);
|
|
33
|
+
hydrateMessageState(conn, config);
|
|
33
34
|
this.seedConfiguredConversations(conn, secret);
|
|
34
35
|
if (conn.timer)
|
|
35
36
|
return;
|
|
@@ -56,6 +57,7 @@ export class WeChatRpaChannelAdapter {
|
|
|
56
57
|
conn.stopped = false;
|
|
57
58
|
conn.config = config;
|
|
58
59
|
hydratePendingReplyState(conn, config);
|
|
60
|
+
hydrateMessageState(conn, config);
|
|
59
61
|
this.seedConfiguredConversations(conn, secret);
|
|
60
62
|
return this.enqueueOperation(conn, () => this.pollOnce(conn, this.readSecret(conn.config)));
|
|
61
63
|
}
|
|
@@ -69,6 +71,7 @@ export class WeChatRpaChannelAdapter {
|
|
|
69
71
|
const conn = this.ensureConnection(config);
|
|
70
72
|
conn.config = config;
|
|
71
73
|
hydratePendingReplyState(conn, config);
|
|
74
|
+
hydrateMessageState(conn, config);
|
|
72
75
|
this.seedConfiguredConversations(conn, secret);
|
|
73
76
|
const conversationName = this.resolveConversationName(config, secret, reply.conversationId);
|
|
74
77
|
if (!conversationName)
|
|
@@ -201,6 +204,7 @@ export class WeChatRpaChannelAdapter {
|
|
|
201
204
|
pendingReplies: new Map(),
|
|
202
205
|
completedPendingReplyKeys: new Set(),
|
|
203
206
|
pendingStatePath: undefined,
|
|
207
|
+
messageStatePath: undefined,
|
|
204
208
|
operation: Promise.resolve(),
|
|
205
209
|
runtimeState: 'idle_waiting',
|
|
206
210
|
consecutiveInterruptions: 0,
|
|
@@ -248,6 +252,7 @@ export class WeChatRpaChannelAdapter {
|
|
|
248
252
|
const message = normalizeWeChatRpaMessage(item);
|
|
249
253
|
if (!message || !conn.deduper.accept(message.messageId))
|
|
250
254
|
continue;
|
|
255
|
+
persistMessageState(conn);
|
|
251
256
|
conn.conversations.set(message.conversationId, message.conversationName);
|
|
252
257
|
conn.lastMessageAt = message.receivedAt;
|
|
253
258
|
const event = {
|
|
@@ -568,6 +573,38 @@ function pendingReplyStatePath(config) {
|
|
|
568
573
|
const id = crypto.createHash('sha256').update(config.id).digest('hex').slice(0, 16);
|
|
569
574
|
return path.join(config.workDir, '.shennian', 'wechat-rpa-pending-replies', `${id}.json`);
|
|
570
575
|
}
|
|
576
|
+
function hydrateMessageState(conn, config) {
|
|
577
|
+
const filePath = messageStatePath(config);
|
|
578
|
+
if (conn.messageStatePath === filePath)
|
|
579
|
+
return;
|
|
580
|
+
conn.messageStatePath = filePath;
|
|
581
|
+
const store = readMessageSeenStore(filePath);
|
|
582
|
+
conn.deduper = new WeChatRpaDeduper((store.messageIds ?? []).filter((id) => typeof id === 'string' && id.length > 0));
|
|
583
|
+
}
|
|
584
|
+
function persistMessageState(conn) {
|
|
585
|
+
if (!conn.messageStatePath)
|
|
586
|
+
return;
|
|
587
|
+
try {
|
|
588
|
+
fs.mkdirSync(path.dirname(conn.messageStatePath), { recursive: true });
|
|
589
|
+
fs.writeFileSync(conn.messageStatePath, JSON.stringify({ version: 1, messageIds: conn.deduper.snapshot() }, null, 2));
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
// Best-effort only; in-memory dedupe still protects the current daemon.
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function readMessageSeenStore(filePath) {
|
|
596
|
+
try {
|
|
597
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
598
|
+
return parsed && parsed.version === 1 ? parsed : { version: 1, messageIds: [] };
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return { version: 1, messageIds: [] };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function messageStatePath(config) {
|
|
605
|
+
const id = crypto.createHash('sha256').update(config.id).digest('hex').slice(0, 16);
|
|
606
|
+
return path.join(config.workDir, '.shennian', 'wechat-rpa-seen-messages', `${id}.json`);
|
|
607
|
+
}
|
|
571
608
|
function noteInterruption(conn, flow, reason) {
|
|
572
609
|
conn.lastInterruptedAt = new Date().toISOString();
|
|
573
610
|
conn.lastError = null;
|
|
@@ -54,6 +54,7 @@ export declare function fetchLatestVersion(): Promise<string>;
|
|
|
54
54
|
* 'major' — major differs
|
|
55
55
|
*/
|
|
56
56
|
export declare function compareVersions(current: string, latest: string): 'none' | 'patch' | 'minor' | 'major';
|
|
57
|
+
export declare function copyPackageRuntimeFiles(src: string, dest: string): void;
|
|
57
58
|
export declare function readUpgradeAttempt(): UpgradeAttempt | null;
|
|
58
59
|
export declare function writeUpgradeAttempt(attempt: UpgradeAttempt): void;
|
|
59
60
|
export declare function clearUpgradeAttempt(): void;
|
|
@@ -10,6 +10,7 @@ const BACKUP_DIR = resolveShennianPath('backup');
|
|
|
10
10
|
const UPGRADE_ATTEMPT_FILE = resolveShennianPath('upgrade-attempt.json');
|
|
11
11
|
const MAX_CRASH_COUNT = 3;
|
|
12
12
|
const BACKUP_TTL_DAYS = 7;
|
|
13
|
+
const NPM_REGISTRY_FALLBACK = 'https://registry.npmjs.org';
|
|
13
14
|
const RETRY_DELAYS_MS = [
|
|
14
15
|
5 * 60_000,
|
|
15
16
|
30 * 60_000,
|
|
@@ -86,8 +87,9 @@ function getGlobalBinScript() {
|
|
|
86
87
|
function backupVersion(version) {
|
|
87
88
|
const pkgDir = getGlobalPkgDir();
|
|
88
89
|
const dest = path.join(BACKUP_DIR, version);
|
|
90
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
89
91
|
fs.mkdirSync(dest, { recursive: true });
|
|
90
|
-
|
|
92
|
+
copyPackageRuntimeFiles(pkgDir, dest);
|
|
91
93
|
}
|
|
92
94
|
function restoreVersion(version) {
|
|
93
95
|
const src = path.join(BACKUP_DIR, version);
|
|
@@ -96,6 +98,14 @@ function restoreVersion(version) {
|
|
|
96
98
|
const pkgDir = getGlobalPkgDir();
|
|
97
99
|
fs.cpSync(src, pkgDir, { recursive: true, force: true });
|
|
98
100
|
}
|
|
101
|
+
export function copyPackageRuntimeFiles(src, dest) {
|
|
102
|
+
for (const entry of ['package.json', 'README.md', 'dist']) {
|
|
103
|
+
const from = path.join(src, entry);
|
|
104
|
+
if (!fs.existsSync(from))
|
|
105
|
+
continue;
|
|
106
|
+
fs.cpSync(from, path.join(dest, entry), { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
99
109
|
function cleanOldBackups() {
|
|
100
110
|
try {
|
|
101
111
|
if (!fs.existsSync(BACKUP_DIR))
|
|
@@ -263,7 +273,7 @@ export async function performUpgrade(targetVersion, onProgress, opts = {}) {
|
|
|
263
273
|
// Step 3: npm install new version
|
|
264
274
|
onProgress({ step: 'installing', version: targetVersion });
|
|
265
275
|
try {
|
|
266
|
-
await
|
|
276
|
+
await installShennianVersion(targetVersion);
|
|
267
277
|
}
|
|
268
278
|
catch (err) {
|
|
269
279
|
// Restore backup and abort
|
|
@@ -309,6 +319,24 @@ export async function performUpgrade(targetVersion, onProgress, opts = {}) {
|
|
|
309
319
|
onProgress({ step: 'restarting', from: currentVersion, to: targetVersion });
|
|
310
320
|
return { ok: true, from: currentVersion, to: targetVersion };
|
|
311
321
|
}
|
|
322
|
+
async function installShennianVersion(targetVersion) {
|
|
323
|
+
try {
|
|
324
|
+
await exec(`npm install -g shennian@${targetVersion}`, { timeout: 120_000, windowsHide: true });
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
const firstMessage = error instanceof Error ? error.message : String(error);
|
|
328
|
+
try {
|
|
329
|
+
await exec(`npm install -g shennian@${targetVersion} --registry ${NPM_REGISTRY_FALLBACK}`, {
|
|
330
|
+
timeout: 120_000,
|
|
331
|
+
windowsHide: true,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
catch (fallbackError) {
|
|
335
|
+
const secondMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
336
|
+
throw new Error(`${firstMessage}\n--- npmjs fallback ---\n${secondMessage}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
312
340
|
export async function checkForUpdate(currentVersion) {
|
|
313
341
|
const current = currentVersion ?? getCurrentVersion();
|
|
314
342
|
const latest = await fetchLatestVersion();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shennian",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.78",
|
|
4
4
|
"description": "Shennian — AI Agent Control Plane CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,16 +33,10 @@
|
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=18"
|
|
35
35
|
},
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "tsc && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
38
|
-
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
39
|
-
"dev": "tsc --watch",
|
|
40
|
-
"prepublishOnly": "pnpm build:publish"
|
|
41
|
-
},
|
|
42
36
|
"dependencies": {
|
|
43
37
|
"@mariozechner/pi-agent-core": "^0.64.0",
|
|
44
38
|
"@sinclair/typebox": "^0.34.49",
|
|
45
|
-
"@shennian/wire": "
|
|
39
|
+
"@shennian/wire": "^0.1.5",
|
|
46
40
|
"chalk": "^5.4.1",
|
|
47
41
|
"commander": "^13.1.0",
|
|
48
42
|
"qrcode-terminal": "^0.12.0",
|
|
@@ -54,5 +48,10 @@
|
|
|
54
48
|
"@types/ws": "^8.18.1",
|
|
55
49
|
"tsx": "^4.19.4",
|
|
56
50
|
"typescript": "^5.9.3"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
54
|
+
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
55
|
+
"dev": "tsc --watch"
|
|
57
56
|
}
|
|
58
|
-
}
|
|
57
|
+
}
|