shennian 0.2.88 → 0.2.90

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 (143) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +22 -0
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/bin/shennian.js +1 -1
  4. package/dist/publish-build-manifest.json +548 -0
  5. package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
  6. package/dist/src/agent-env.js +4 -105
  7. package/dist/src/agents/adapter.d.ts +6 -0
  8. package/dist/src/agents/adapter.js +1 -19
  9. package/dist/src/agents/claude.js +8 -305
  10. package/dist/src/agents/codex-control.d.ts +35 -0
  11. package/dist/src/agents/codex-control.js +2 -0
  12. package/dist/src/agents/codex-utils.js +7 -200
  13. package/dist/src/agents/codex.d.ts +8 -0
  14. package/dist/src/agents/codex.js +15 -863
  15. package/dist/src/agents/command-spec.js +2 -413
  16. package/dist/src/agents/config-status.js +1 -226
  17. package/dist/src/agents/cursor.js +1 -249
  18. package/dist/src/agents/custom.js +4 -271
  19. package/dist/src/agents/detect.js +1 -56
  20. package/dist/src/agents/external-channel-instructions.js +10 -94
  21. package/dist/src/agents/gemini.js +1 -173
  22. package/dist/src/agents/manager.js +13 -157
  23. package/dist/src/agents/model-registry/cache.js +1 -37
  24. package/dist/src/agents/model-registry/discovery.js +2 -187
  25. package/dist/src/agents/model-registry/parsers.js +4 -447
  26. package/dist/src/agents/model-registry/runner.js +1 -30
  27. package/dist/src/agents/model-registry/service.js +1 -78
  28. package/dist/src/agents/model-registry/types.js +1 -8
  29. package/dist/src/agents/model-registry.js +1 -18
  30. package/dist/src/agents/openclaw.js +2 -275
  31. package/dist/src/agents/opencode.js +1 -231
  32. package/dist/src/agents/pi-context.js +12 -217
  33. package/dist/src/agents/pi.js +14 -723
  34. package/dist/src/agents/platform-instructions.js +9 -54
  35. package/dist/src/channels/base.d.ts +4 -1
  36. package/dist/src/channels/base.js +1 -3
  37. package/dist/src/channels/registry.js +1 -30
  38. package/dist/src/channels/reply-split.js +10 -89
  39. package/dist/src/channels/runtime.d.ts +1 -0
  40. package/dist/src/channels/runtime.js +5 -533
  41. package/dist/src/channels/secret-registry.d.ts +1 -0
  42. package/dist/src/channels/secret-registry.js +1 -46
  43. package/dist/src/channels/websocket.js +8 -378
  44. package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
  45. package/dist/src/channels/wechat-channel/anchor.js +1 -0
  46. package/dist/src/channels/wechat-channel/client.d.ts +74 -0
  47. package/dist/src/channels/wechat-channel/client.js +1 -0
  48. package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
  49. package/dist/src/channels/wechat-channel/cooldown.js +1 -0
  50. package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
  51. package/dist/src/channels/wechat-channel/fingerprint.js +1 -0
  52. package/dist/src/channels/wechat-channel/helper-assets.d.ts +37 -0
  53. package/dist/src/channels/wechat-channel/helper-assets.js +1 -0
  54. package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
  55. package/dist/src/channels/wechat-channel/helper-client.js +3 -0
  56. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
  57. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -0
  58. package/dist/src/channels/wechat-channel/index.d.ts +17 -0
  59. package/dist/src/channels/wechat-channel/index.js +1 -0
  60. package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
  61. package/dist/src/channels/wechat-channel/ledger.js +1 -0
  62. package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
  63. package/dist/src/channels/wechat-channel/media-resolver.js +1 -0
  64. package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
  65. package/dist/src/channels/wechat-channel/message-key.js +1 -0
  66. package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
  67. package/dist/src/channels/wechat-channel/observer.js +1 -0
  68. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +69 -0
  69. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -0
  70. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  71. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  72. package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
  73. package/dist/src/channels/wechat-channel/preflight.js +1 -0
  74. package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
  75. package/dist/src/channels/wechat-channel/runner.js +1 -0
  76. package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
  77. package/dist/src/channels/wechat-channel/runtime.js +1 -0
  78. package/dist/src/channels/wechat-channel/scheduler.d.ts +35 -0
  79. package/dist/src/channels/wechat-channel/scheduler.js +1 -0
  80. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  81. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  82. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  83. package/dist/src/channels/wechat-rpa.d.ts +21 -0
  84. package/dist/src/channels/wechat-rpa.js +6 -1022
  85. package/dist/src/channels/wecom.js +4 -357
  86. package/dist/src/commands/agent.js +6 -131
  87. package/dist/src/commands/daemon-windows.js +8 -48
  88. package/dist/src/commands/daemon.js +19 -1013
  89. package/dist/src/commands/external-attachments.js +1 -51
  90. package/dist/src/commands/external.js +1 -137
  91. package/dist/src/commands/manager.js +2 -389
  92. package/dist/src/commands/pair-qr.js +1 -6
  93. package/dist/src/commands/pair.js +9 -287
  94. package/dist/src/commands/tools.js +1 -34
  95. package/dist/src/commands/upgrade.js +1 -198
  96. package/dist/src/config/index.js +1 -35
  97. package/dist/src/daemon-log.js +6 -58
  98. package/dist/src/env-path.js +1 -64
  99. package/dist/src/fs/boundary.js +1 -126
  100. package/dist/src/fs/handler.js +1 -130
  101. package/dist/src/fs/security.js +1 -32
  102. package/dist/src/fs/text-decoder.d.ts +10 -0
  103. package/dist/src/fs/text-decoder.js +1 -0
  104. package/dist/src/index.js +2 -404
  105. package/dist/src/log-reporter.js +1 -16
  106. package/dist/src/manager/prompt.js +29 -34
  107. package/dist/src/manager/registry.js +2 -269
  108. package/dist/src/manager/runtime.js +19 -1003
  109. package/dist/src/native-fusion/config.js +1 -5
  110. package/dist/src/native-fusion/opencode-parser.js +3 -123
  111. package/dist/src/native-fusion/parser-common.js +8 -264
  112. package/dist/src/native-fusion/parsers.js +8 -729
  113. package/dist/src/native-fusion/service.d.ts +10 -0
  114. package/dist/src/native-fusion/service.js +2 -198
  115. package/dist/src/native-fusion/state.js +1 -22
  116. package/dist/src/native-fusion/types.js +1 -1
  117. package/dist/src/region.js +1 -88
  118. package/dist/src/relay/client.js +1 -343
  119. package/dist/src/session/archive-zip.js +1 -220
  120. package/dist/src/session/handlers/agent-config.js +1 -150
  121. package/dist/src/session/handlers/agents.js +1 -55
  122. package/dist/src/session/handlers/chat.js +2 -733
  123. package/dist/src/session/handlers/control.js +1 -55
  124. package/dist/src/session/handlers/fs.js +1 -747
  125. package/dist/src/session/handlers/session-refresh.js +1 -35
  126. package/dist/src/session/handlers/skills.js +1 -121
  127. package/dist/src/session/handlers/title.js +1 -60
  128. package/dist/src/session/handlers/tool-detail.d.ts +3 -0
  129. package/dist/src/session/handlers/tool-detail.js +1 -0
  130. package/dist/src/session/manager.d.ts +3 -0
  131. package/dist/src/session/manager.js +1 -261
  132. package/dist/src/session/projection.js +1 -54
  133. package/dist/src/session/queue.js +4 -317
  134. package/dist/src/session/remote-attachments.js +1 -72
  135. package/dist/src/session/store.js +3 -109
  136. package/dist/src/session/types.d.ts +4 -0
  137. package/dist/src/session/types.js +1 -4
  138. package/dist/src/skills/registry.js +15 -148
  139. package/dist/src/skills/setup.js +1 -101
  140. package/dist/src/tools/markdown-to-pdf.js +10 -346
  141. package/dist/src/upgrade/engine.js +3 -347
  142. package/package.json +3 -2
  143. package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
@@ -1,4 +1,5 @@
1
1
  import type { CliRelayClient } from '../relay/client.js';
2
+ import type { SessionActivitySnapshot } from '@shennian/wire';
2
3
  export declare class NativeSessionFusionService {
3
4
  private client;
4
5
  private timer;
@@ -20,6 +21,15 @@ export declare class NativeSessionFusionService {
20
21
  noteManagedSourceSession(sessionId: string, agentType: string, sourceSessionKey: string | null): void;
21
22
  private pruneState;
22
23
  scanNow(): Promise<void>;
24
+ getCodexThreadActivity(params: {
25
+ sessionId: string;
26
+ threadId: string;
27
+ workDir?: string;
28
+ }): Promise<SessionActivitySnapshot | null | undefined>;
29
+ interruptCodexThread(params: {
30
+ threadId: string;
31
+ workDir?: string;
32
+ }): Promise<boolean>;
23
33
  private runScan;
24
34
  private tryClaimManagedEcho;
25
35
  private isSuppressed;
@@ -1,198 +1,2 @@
1
- // @arch docs/architecture/cli/native-session-fusion.md
2
- // @test src/__tests__/native-fusion-parsers.test.ts
3
- import { randomUUID } from 'node:crypto';
4
- import fs from 'node:fs';
5
- import { listClaudeTranscriptFiles, listCodexRolloutFiles, listOpenCodeSessionFiles, parseClaudeTranscriptChunk, parseCodexRolloutChunk, parseOpenCodeSessionFile, } from './parsers.js';
6
- import { loadNativeScannerState, saveNativeScannerState } from './state.js';
7
- const SCAN_INTERVAL_MS = 60_000;
8
- const CLAIM_TTL_MS = SCAN_INTERVAL_MS * 2;
9
- const MANAGED_RUN_SUPPRESSION_TTL_MS = 30 * 60_000;
10
- const MAX_BATCH_SIZE = 20;
11
- function normalizeText(text) {
12
- return text.replace(/\r\n/g, '\n').trim();
13
- }
14
- function isCodexRolloutPath(filePath) {
15
- return filePath.split(/[\\/]+/).includes('.codex');
16
- }
17
- export class NativeSessionFusionService {
18
- client;
19
- timer = null;
20
- scanPromise = null;
21
- pendingClaims = new Map();
22
- suppressionWindows = new Map();
23
- constructor(client) {
24
- this.client = client;
25
- }
26
- start() {
27
- if (this.timer)
28
- return;
29
- this.timer = setInterval(() => {
30
- void this.scanNow().catch((error) => {
31
- console.error('[native-fusion] periodic scan failed', error);
32
- });
33
- }, SCAN_INTERVAL_MS);
34
- }
35
- stop() {
36
- if (this.timer)
37
- clearInterval(this.timer);
38
- this.timer = null;
39
- }
40
- handleConnected() {
41
- this.start();
42
- void this.scanNow().catch((error) => {
43
- console.error('[native-fusion] initial scan failed', error);
44
- });
45
- }
46
- registerManagedSend(params) {
47
- if (!params.canonicalMessageId)
48
- return;
49
- const sourceAgentType = params.sourceAgentType ?? params.agentType;
50
- if (sourceAgentType !== 'codex' && sourceAgentType !== 'claude' && sourceAgentType !== 'opencode')
51
- return;
52
- this.pendingClaims.set(params.sessionId, {
53
- sessionId: params.sessionId,
54
- agentType: params.agentType,
55
- sourceAgentType,
56
- canonicalMessageId: params.canonicalMessageId,
57
- text: normalizeText(params.text),
58
- createdAt: Date.now(),
59
- expiresAt: Date.now() + CLAIM_TTL_MS,
60
- sourceSessionKey: params.sourceSessionKey ?? null,
61
- });
62
- }
63
- noteManagedSourceSession(sessionId, agentType, sourceSessionKey) {
64
- if (!sourceSessionKey)
65
- return;
66
- const claim = this.pendingClaims.get(sessionId);
67
- if (claim && claim.sourceAgentType === agentType) {
68
- claim.sourceSessionKey = sourceSessionKey;
69
- }
70
- }
71
- pruneState() {
72
- const now = Date.now();
73
- for (const [sessionId, claim] of this.pendingClaims.entries()) {
74
- if (claim.expiresAt <= now)
75
- this.pendingClaims.delete(sessionId);
76
- }
77
- for (const [key, window] of this.suppressionWindows.entries()) {
78
- if (window.endsAt <= now)
79
- this.suppressionWindows.delete(key);
80
- }
81
- }
82
- async scanNow() {
83
- if (this.client.getState() !== 'connected')
84
- return;
85
- if (this.scanPromise)
86
- return this.scanPromise;
87
- this.scanPromise = this.runScan().finally(() => {
88
- this.scanPromise = null;
89
- });
90
- return this.scanPromise;
91
- }
92
- async runScan() {
93
- this.pruneState();
94
- const state = loadNativeScannerState();
95
- const nextFilesState = { ...state.files };
96
- const batches = [];
97
- const files = [...listCodexRolloutFiles(), ...listClaudeTranscriptFiles(), ...listOpenCodeSessionFiles()];
98
- for (const filePath of files) {
99
- const stat = fs.statSync(filePath);
100
- const current = state.files[filePath];
101
- if (!current) {
102
- nextFilesState[filePath] = {
103
- offset: stat.size,
104
- mtimeMs: stat.mtimeMs,
105
- };
106
- continue;
107
- }
108
- const isCodex = isCodexRolloutPath(filePath);
109
- const isOpenCode = filePath.endsWith('.opencode-session.json');
110
- const startOffset = isOpenCode && current?.mtimeMs !== stat.mtimeMs
111
- ? current.offset
112
- : current?.offset ?? 0;
113
- const parsed = isOpenCode
114
- ? parseOpenCodeSessionFile(filePath, startOffset)
115
- : isCodex
116
- ? parseCodexRolloutChunk(filePath, startOffset)
117
- : parseClaudeTranscriptChunk(filePath, startOffset);
118
- nextFilesState[filePath] = {
119
- offset: parsed.nextOffset,
120
- mtimeMs: stat.mtimeMs,
121
- };
122
- for (const event of parsed.events) {
123
- const claimed = this.tryClaimManagedEcho(event);
124
- if (claimed) {
125
- batches.push(claimed);
126
- continue;
127
- }
128
- if (this.isSuppressed(event))
129
- continue;
130
- batches.push(event);
131
- }
132
- }
133
- for (let index = 0; index < batches.length; index += MAX_BATCH_SIZE) {
134
- const chunk = batches.slice(index, index + MAX_BATCH_SIZE);
135
- if (chunk.length === 0)
136
- continue;
137
- const wireEvents = chunk.map(({ repairHint: _repairHint, ...event }) => event);
138
- await this.client.sendBufferedEvent({
139
- type: 'event',
140
- event: 'native.session.events',
141
- id: `native-fusion-${randomUUID()}`,
142
- payload: { events: wireEvents },
143
- }, 60_000);
144
- }
145
- state.files = nextFilesState;
146
- saveNativeScannerState(state);
147
- }
148
- tryClaimManagedEcho(event) {
149
- const now = Date.now();
150
- for (const [sessionId, claim] of this.pendingClaims.entries()) {
151
- if (claim.sourceAgentType !== event.agentType)
152
- continue;
153
- if (claim.expiresAt <= now)
154
- continue;
155
- if (event.role !== 'user')
156
- continue;
157
- if (!claim.sourceSessionKey)
158
- continue;
159
- if (claim.sourceSessionKey !== event.sourceSessionKey)
160
- continue;
161
- if (Math.abs(event.ts - claim.createdAt) > CLAIM_TTL_MS)
162
- continue;
163
- if (normalizeText(event.payload) !== claim.text)
164
- continue;
165
- this.pendingClaims.delete(sessionId);
166
- this.suppressionWindows.set(`${event.agentType}:${event.sourceSessionKey}`, {
167
- sessionId,
168
- agentType: event.agentType,
169
- sourceSessionKey: event.sourceSessionKey,
170
- startedAt: event.ts,
171
- endsAt: Math.max(now, event.ts) + MANAGED_RUN_SUPPRESSION_TTL_MS,
172
- });
173
- return {
174
- ...event,
175
- sourceMode: 'managed_echo_claim',
176
- claimCanonicalMessageId: claim.canonicalMessageId,
177
- };
178
- }
179
- return null;
180
- }
181
- isSuppressed(event) {
182
- const key = `${event.agentType}:${event.sourceSessionKey}`;
183
- const window = this.suppressionWindows.get(key);
184
- if (!window)
185
- return false;
186
- if (event.ts < window.startedAt)
187
- return false;
188
- if (event.ts > window.endsAt) {
189
- this.suppressionWindows.delete(key);
190
- return false;
191
- }
192
- if (event.role === 'user') {
193
- this.suppressionWindows.delete(key);
194
- return false;
195
- }
196
- return true;
197
- }
198
- }
1
+ import{randomUUID as g}from"node:crypto";import w from"node:fs";import{listClaudeTranscriptFiles as C,listCodexRolloutFiles as I,listOpenCodeSessionFiles as M,parseClaudeTranscriptChunk as A,parseCodexRolloutChunk as T,parseOpenCodeSessionFile as x}from"./parsers.js";import{probeCodexThreadActivity as _,interruptCodexThread as P}from"../agents/codex-control.js";import{loadNativeScannerState as v,saveNativeScannerState as D}from"./state.js";const h=6e4,m=h*2,N=30*6e4,S=20;function y(a){return a.replace(/\r\n/g,`
2
+ `).trim()}function k(a){return a.split(/[\\/]+/).includes(".codex")}class R{client;timer=null;scanPromise=null;pendingClaims=new Map;suppressionWindows=new Map;constructor(e){this.client=e}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},h))}stop(){this.timer&&clearInterval(this.timer),this.timer=null}handleConnected(){this.start(),this.scanNow().catch(e=>{console.error("[native-fusion] initial scan failed",e)})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;s!=="codex"&&s!=="claude"&&s!=="opencode"||this.pendingClaims.set(e.sessionId,{sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,text:y(e.text),createdAt:Date.now(),expiresAt:Date.now()+m,sourceSessionKey:e.sourceSessionKey??null})}noteManagedSourceSession(e,s,t){if(!t)return;const i=this.pendingClaims.get(e);i&&i.sourceAgentType===s&&(i.sourceSessionKey=t)}pruneState(){const e=Date.now();for(const[s,t]of this.pendingClaims.entries())t.expiresAt<=e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.endsAt<=e&&this.suppressionWindows.delete(s)}async scanNow(){if(this.client.getState()==="connected")return this.scanPromise?this.scanPromise:(this.scanPromise=this.runScan().finally(()=>{this.scanPromise=null}),this.scanPromise)}async getCodexThreadActivity(e){const s=await _({threadId:e.threadId,workDir:e.workDir});if(!s)return;if(!s.active||!s.runPhase)return null;const t=new Date().toISOString();return{sessionId:e.sessionId,runId:s.turnId??`codex:${e.threadId}`,runPhase:s.runPhase,startedAt:t,updatedAt:t,canStop:s.canStop}}async interruptCodexThread(e){return!!await P({threadId:e.threadId,workDir:e.workDir})}async runScan(){this.pruneState();const e=v(),s={...e.files},t=[],i=[...I(),...C(),...M()];for(const n of i){const o=w.statSync(n),r=e.files[n];if(!r){s[n]={offset:o.size,mtimeMs:o.mtimeMs};continue}const l=k(n),c=n.endsWith(".opencode-session.json"),d=c&&r?.mtimeMs!==o.mtimeMs?r.offset:r?.offset??0,f=c?x(n,d):l?T(n,d):A(n,d);s[n]={offset:f.nextOffset,mtimeMs:o.mtimeMs};for(const u of f.events){const p=this.tryClaimManagedEcho(u);if(p){t.push(p);continue}this.isSuppressed(u)||t.push(u)}}for(let n=0;n<t.length;n+=S){const o=t.slice(n,n+S);if(o.length===0)continue;const r=o.map(({repairHint:l,...c})=>c);await this.client.sendBufferedEvent({type:"event",event:"native.session.events",id:`native-fusion-${g()}`,payload:{events:r}},6e4)}e.files=s,D(e)}tryClaimManagedEcho(e){const s=Date.now();for(const[t,i]of this.pendingClaims.entries())if(i.sourceAgentType===e.agentType&&!(i.expiresAt<=s)&&e.role==="user"&&i.sourceSessionKey&&i.sourceSessionKey===e.sourceSessionKey&&!(Math.abs(e.ts-i.createdAt)>m)&&y(e.payload)===i.text)return this.pendingClaims.delete(t),this.suppressionWindows.set(`${e.agentType}:${e.sourceSessionKey}`,{sessionId:t,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+N}),{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:i.canonicalMessageId};return null}isSuppressed(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.suppressionWindows.get(s);return!t||e.ts<t.startedAt?!1:e.ts>t.endsAt?(this.suppressionWindows.delete(s),!1):e.role==="user"?(this.suppressionWindows.delete(s),!1):!0}}export{R as NativeSessionFusionService};
@@ -1,22 +1 @@
1
- // @arch docs/architecture/cli/native-session-fusion.md
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { resolveShennianPath } from '../config/index.js';
5
- const STATE_FILE = resolveShennianPath('native-fusion-state.json');
6
- export function loadNativeScannerState() {
7
- try {
8
- const raw = fs.readFileSync(STATE_FILE, 'utf-8');
9
- const parsed = JSON.parse(raw);
10
- if (parsed && typeof parsed === 'object' && parsed.files && typeof parsed.files === 'object') {
11
- return parsed;
12
- }
13
- }
14
- catch {
15
- // ignore missing or malformed state
16
- }
17
- return { files: {} };
18
- }
19
- export function saveNativeScannerState(state) {
20
- fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
21
- fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
22
- }
1
+ import r from"node:fs";import i from"node:path";import{resolveShennianPath as o}from"../config/index.js";const n=o("native-fusion-state.json");function f(){try{const t=r.readFileSync(n,"utf-8"),e=JSON.parse(t);if(e&&typeof e=="object"&&e.files&&typeof e.files=="object")return e}catch{}return{files:{}}}function S(t){r.mkdirSync(i.dirname(n),{recursive:!0}),r.writeFileSync(n,JSON.stringify(t,null,2))}export{f as loadNativeScannerState,S as saveNativeScannerState};
@@ -1 +1 @@
1
- export {};
1
+
@@ -1,88 +1 @@
1
- // @arch docs/architecture/deployment.md
2
- // @test src/__tests__/config.test.ts
3
- import chalk from 'chalk';
4
- export const SERVERS = {
5
- cn: { url: 'https://shennian.net', label: 'China (shennian.net)' },
6
- // Keep the historical key so old configs and protocol payloads remain
7
- // compatible. Production is single-domain and resolves both keys to cn.
8
- global: { url: 'https://shennian.net', label: 'China (shennian.net)' },
9
- };
10
- const CN_TIMEZONES = [
11
- 'Asia/Shanghai', 'Asia/Chongqing', 'Asia/Chungking',
12
- 'Asia/Harbin', 'Asia/Urumqi', 'PRC',
13
- ];
14
- function inferRegionByTimezone() {
15
- try {
16
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
17
- if (CN_TIMEZONES.includes(tz))
18
- return 'cn';
19
- }
20
- catch { /* Intl unavailable */ }
21
- return 'cn';
22
- }
23
- async function detectRegionByIP(timeoutMs = 1500) {
24
- const controller = new AbortController();
25
- const timer = setTimeout(() => controller.abort(), timeoutMs);
26
- try {
27
- const res = await fetch(`${SERVERS.cn.url}/geo`, { signal: controller.signal });
28
- const data = await res.json();
29
- return data.country === 'CN' ? 'cn' : 'cn';
30
- }
31
- catch {
32
- return null;
33
- }
34
- finally {
35
- clearTimeout(timer);
36
- }
37
- }
38
- async function verifyReachable(url, timeoutMs = 3000) {
39
- const controller = new AbortController();
40
- const timer = setTimeout(() => controller.abort(), timeoutMs);
41
- try {
42
- await fetch(`${url}/health`, { signal: controller.signal });
43
- return true;
44
- }
45
- catch {
46
- return false;
47
- }
48
- finally {
49
- clearTimeout(timer);
50
- }
51
- }
52
- /**
53
- * Detect the best server region and return its URL.
54
- *
55
- * Strategy: timezone as primary signal, IP geolocation as supplement.
56
- * - Chinese timezone → cn immediately (covers servers with UTC too via IP fallback).
57
- * - Non-Chinese timezone → try IP to detect if physically in China.
58
- */
59
- export async function detectAndChooseServer() {
60
- console.log(chalk.gray(' Detecting best server...'));
61
- const tzRegion = inferRegionByTimezone();
62
- if (tzRegion === 'cn') {
63
- const reachable = await verifyReachable(SERVERS.cn.url);
64
- if (reachable) {
65
- console.log(chalk.gray(` Using ${SERVERS.cn.label} (timezone: China)`));
66
- return SERVERS.cn.url;
67
- }
68
- console.error(chalk.red(' Error: cannot reach any server.'));
69
- process.exit(1);
70
- }
71
- // Non-CN timezone: check IP to catch servers in China with UTC timezone
72
- const ipRegion = await detectRegionByIP();
73
- const region = ipRegion ?? tzRegion;
74
- const reachable = await verifyReachable(SERVERS[region].url);
75
- if (reachable) {
76
- const source = ipRegion ? 'IP geolocation' : 'timezone';
77
- console.log(chalk.gray(` Using ${SERVERS[region].label} (${source})`));
78
- return SERVERS[region].url;
79
- }
80
- console.error(chalk.red(' Error: cannot reach any server.'));
81
- process.exit(1);
82
- }
83
- export function urlToRegion(url) {
84
- return url.includes('shennian.net') ? 'cn' : 'global';
85
- }
86
- export function regionToUrl(region) {
87
- return SERVERS[region].url;
88
- }
1
+ import o from"chalk";const r={cn:{url:"https://shennian.net",label:"China (shennian.net)"},global:{url:"https://shennian.net",label:"China (shennian.net)"}},l=["Asia/Shanghai","Asia/Chongqing","Asia/Chungking","Asia/Harbin","Asia/Urumqi","PRC"];function s(){try{const n=Intl.DateTimeFormat().resolvedOptions().timeZone;if(l.includes(n))return"cn"}catch{}return"cn"}async function u(n=1500){const t=new AbortController,e=setTimeout(()=>t.abort(),n);try{return(await(await fetch(`${r.cn.url}/geo`,{signal:t.signal})).json()).country==="CN","cn"}catch{return null}finally{clearTimeout(e)}}async function i(n,t=3e3){const e=new AbortController,a=setTimeout(()=>e.abort(),t);try{return await fetch(`${n}/health`,{signal:e.signal}),!0}catch{return!1}finally{clearTimeout(a)}}async function g(){console.log(o.gray(" Detecting best server..."));const n=s();if(n==="cn"){if(await i(r.cn.url))return console.log(o.gray(` Using ${r.cn.label} (timezone: China)`)),r.cn.url;console.error(o.red(" Error: cannot reach any server.")),process.exit(1)}const t=await u(),e=t??n;if(await i(r[e].url)){const c=t?"IP geolocation":"timezone";return console.log(o.gray(` Using ${r[e].label} (${c})`)),r[e].url}console.error(o.red(" Error: cannot reach any server.")),process.exit(1)}function f(n){return n.includes("shennian.net")?"cn":"global"}function b(n){return r[n].url}export{r as SERVERS,g as detectAndChooseServer,b as regionToUrl,f as urlToRegion};