shennian 0.2.64 → 0.2.66
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/src/agent-env.js +2 -0
- package/dist/src/agents/command-spec.js +5 -0
- package/dist/src/agents/config-status.d.ts +5 -4
- package/dist/src/agents/config-status.js +30 -9
- package/dist/src/agents/manager.js +11 -1
- package/dist/src/agents/pi-context.d.ts +1 -1
- package/dist/src/agents/pi-context.js +4 -3
- package/dist/src/agents/pi.d.ts +1 -0
- package/dist/src/agents/pi.js +34 -5
- package/dist/src/commands/daemon.d.ts +7 -0
- package/dist/src/commands/daemon.js +28 -20
- package/dist/src/commands/external-attachments.d.ts +9 -0
- package/dist/src/commands/external-attachments.js +52 -0
- package/dist/src/commands/external.js +4 -52
- package/dist/src/commands/manager.js +49 -6
- package/dist/src/commands/tools.d.ts +2 -0
- package/dist/src/commands/tools.js +34 -0
- package/dist/src/commands/upgrade.js +1 -1
- package/dist/src/fs/boundary.js +7 -2
- package/dist/src/index.js +2 -0
- package/dist/src/manager/prompt.d.ts +1 -1
- package/dist/src/manager/prompt.js +10 -4
- package/dist/src/manager/registry.d.ts +4 -0
- package/dist/src/manager/registry.js +2 -0
- package/dist/src/manager/runtime.d.ts +8 -1
- package/dist/src/manager/runtime.js +35 -8
- package/dist/src/session/handlers/agent-config.js +3 -3
- package/dist/src/session/handlers/chat.js +33 -12
- package/dist/src/session/handlers/fs.d.ts +1 -0
- package/dist/src/session/handlers/fs.js +76 -2
- package/dist/src/session/manager.js +4 -1
- package/dist/src/session/queue.js +18 -2
- package/dist/src/session/remote-attachments.d.ts +15 -0
- package/dist/src/session/remote-attachments.js +72 -0
- package/dist/src/tools/markdown-to-pdf.d.ts +20 -0
- package/dist/src/tools/markdown-to-pdf.js +303 -0
- package/dist/src/upgrade/engine.js +5 -5
- package/package.json +2 -2
package/dist/src/agent-env.js
CHANGED
|
@@ -41,6 +41,7 @@ function readPosixShellEnv() {
|
|
|
41
41
|
encoding: 'utf-8',
|
|
42
42
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
43
|
timeout: 1500,
|
|
44
|
+
windowsHide: true,
|
|
44
45
|
});
|
|
45
46
|
const parsed = typeof result.stdout === 'string' ? parseEnvJson(result.stdout) : null;
|
|
46
47
|
if (parsed)
|
|
@@ -61,6 +62,7 @@ Write-Output '${SHELL_ENV_END}'
|
|
|
61
62
|
encoding: 'utf-8',
|
|
62
63
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
63
64
|
timeout: 1500,
|
|
65
|
+
windowsHide: true,
|
|
64
66
|
});
|
|
65
67
|
return typeof result.stdout === 'string' ? parseEnvJson(result.stdout) : null;
|
|
66
68
|
}
|
|
@@ -109,6 +109,7 @@ function lookupCommandPaths(command) {
|
|
|
109
109
|
encoding: 'utf-8',
|
|
110
110
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
111
|
timeout: 3000,
|
|
112
|
+
windowsHide: true,
|
|
112
113
|
env: {
|
|
113
114
|
...process.env,
|
|
114
115
|
PATH: isWindows ? process.env.PATH : buildFallbackPathEnv(process.env.PATH, command),
|
|
@@ -192,6 +193,7 @@ function canUseWslBridge(command) {
|
|
|
192
193
|
const result = spawnSync(wslPath, ['-e', 'sh', '-lc', `command -v ${quotePosixArg(command)} >/dev/null 2>&1`], {
|
|
193
194
|
stdio: 'ignore',
|
|
194
195
|
timeout: 5000,
|
|
196
|
+
windowsHide: true,
|
|
195
197
|
});
|
|
196
198
|
const ok = result.status === 0;
|
|
197
199
|
wslAvailabilityCache.set(command, ok);
|
|
@@ -274,6 +276,7 @@ export function spawnResolvedCommand(spec, runtimeArgs, options = {}) {
|
|
|
274
276
|
return spawn(launch.command, launch.args, {
|
|
275
277
|
...options,
|
|
276
278
|
cwd: launch.cwd,
|
|
279
|
+
windowsHide: options.windowsHide ?? true,
|
|
277
280
|
...(getProcessPlatform() === 'win32' && spec.kind === 'cmd-shim'
|
|
278
281
|
? { windowsVerbatimArguments: true }
|
|
279
282
|
: {}),
|
|
@@ -287,6 +290,7 @@ export function spawnResolvedCommandSync(spec, runtimeArgs, options = {}) {
|
|
|
287
290
|
return spawnSync(launch.command, launch.args, {
|
|
288
291
|
...options,
|
|
289
292
|
cwd: launch.cwd,
|
|
293
|
+
windowsHide: options.windowsHide ?? true,
|
|
290
294
|
...(getProcessPlatform() === 'win32' && spec.kind === 'cmd-shim'
|
|
291
295
|
? { windowsVerbatimArguments: true }
|
|
292
296
|
: {}),
|
|
@@ -356,6 +360,7 @@ export function spawnCommandString(commandString, runtimeArgs, options = {}) {
|
|
|
356
360
|
return spawn(launch.command, launch.args, {
|
|
357
361
|
...options,
|
|
358
362
|
cwd: launch.cwd,
|
|
363
|
+
windowsHide: options.windowsHide ?? true,
|
|
359
364
|
env: {
|
|
360
365
|
...options.env,
|
|
361
366
|
PATH: buildFallbackPathEnv(options.env?.PATH ?? process.env.PATH, parts[0]),
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import type { AgentConfigSummary, AgentType } from '@shennian/wire';
|
|
2
|
+
export type ConfigurableAgent = 'codex' | 'claude' | 'pi';
|
|
2
3
|
export type AgentProviderConfigRecord = {
|
|
3
|
-
agent:
|
|
4
|
+
agent: ConfigurableAgent;
|
|
4
5
|
baseUrl?: string;
|
|
5
6
|
token?: string;
|
|
6
7
|
updatedAt: string;
|
|
7
8
|
};
|
|
8
|
-
export declare function getManagedAgentProviderConfig(agent:
|
|
9
|
+
export declare function getManagedAgentProviderConfig(agent: ConfigurableAgent): AgentProviderConfigRecord | undefined;
|
|
9
10
|
export declare function upsertManagedAgentProviderConfig(input: {
|
|
10
|
-
agent:
|
|
11
|
+
agent: ConfigurableAgent;
|
|
11
12
|
baseUrl?: string;
|
|
12
13
|
token?: string;
|
|
13
14
|
}): AgentProviderConfigRecord;
|
|
14
|
-
export declare function deleteManagedAgentProviderConfig(agent:
|
|
15
|
+
export declare function deleteManagedAgentProviderConfig(agent: ConfigurableAgent): void;
|
|
15
16
|
export declare function buildManagedAgentEnv(agent: AgentType): NodeJS.ProcessEnv;
|
|
16
17
|
export declare function getAgentConfigSummary(agent: AgentType, env?: NodeJS.ProcessEnv): AgentConfigSummary | undefined;
|
|
17
18
|
export declare function maskToken(token: string): string;
|
|
@@ -4,9 +4,12 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { readLatestUserEnv } from '../agent-env.js';
|
|
7
|
-
import { resolveShennianPath } from '../config/index.js';
|
|
7
|
+
import { loadConfig, resolveShennianPath } from '../config/index.js';
|
|
8
8
|
const MANAGED_CONFIG_PATH = resolveShennianPath('agent-provider-config.json');
|
|
9
9
|
const TOKEN_SUFFIX_LENGTH = 4;
|
|
10
|
+
function isConfigurableAgent(agent) {
|
|
11
|
+
return agent === 'codex' || agent === 'claude' || agent === 'pi';
|
|
12
|
+
}
|
|
10
13
|
const AGENT_ENV_KEYS = {
|
|
11
14
|
codex: {
|
|
12
15
|
baseUrl: ['OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENAI_API_URL'],
|
|
@@ -16,6 +19,10 @@ const AGENT_ENV_KEYS = {
|
|
|
16
19
|
baseUrl: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_API_URL'],
|
|
17
20
|
token: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
|
|
18
21
|
},
|
|
22
|
+
pi: {
|
|
23
|
+
baseUrl: ['DASHSCOPE_BASE_URL', 'DASHSCOPE_API_BASE', 'DASHSCOPE_API_URL'],
|
|
24
|
+
token: ['DASHSCOPE_API_KEY', 'DASHSCOPE_TOKEN'],
|
|
25
|
+
},
|
|
19
26
|
};
|
|
20
27
|
export function getManagedAgentProviderConfig(agent) {
|
|
21
28
|
return loadManagedConfig().configs[agent];
|
|
@@ -39,7 +46,7 @@ export function deleteManagedAgentProviderConfig(agent) {
|
|
|
39
46
|
saveManagedConfig(file);
|
|
40
47
|
}
|
|
41
48
|
export function buildManagedAgentEnv(agent) {
|
|
42
|
-
if (agent
|
|
49
|
+
if (!isConfigurableAgent(agent))
|
|
43
50
|
return {};
|
|
44
51
|
const config = getManagedAgentProviderConfig(agent);
|
|
45
52
|
if (!config)
|
|
@@ -50,13 +57,19 @@ export function buildManagedAgentEnv(agent) {
|
|
|
50
57
|
OPENAI_API_KEY: config.token,
|
|
51
58
|
});
|
|
52
59
|
}
|
|
60
|
+
if (agent === 'claude') {
|
|
61
|
+
return compactEnv({
|
|
62
|
+
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
63
|
+
ANTHROPIC_API_KEY: config.token,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
53
66
|
return compactEnv({
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
DASHSCOPE_BASE_URL: config.baseUrl,
|
|
68
|
+
DASHSCOPE_API_KEY: config.token,
|
|
56
69
|
});
|
|
57
70
|
}
|
|
58
71
|
export function getAgentConfigSummary(agent, env = readLatestUserEnv()) {
|
|
59
|
-
if (agent
|
|
72
|
+
if (!isConfigurableAgent(agent))
|
|
60
73
|
return undefined;
|
|
61
74
|
const managed = getManagedAgentProviderConfig(agent);
|
|
62
75
|
if (managed?.token || managed?.baseUrl) {
|
|
@@ -141,9 +154,17 @@ function firstEnv(env, keys) {
|
|
|
141
154
|
}
|
|
142
155
|
function detectFromKnownConfigFiles(agent) {
|
|
143
156
|
const home = os.homedir();
|
|
157
|
+
if (agent === 'pi') {
|
|
158
|
+
const config = loadConfig();
|
|
159
|
+
const token = config.apiKeys?.dashscope?.trim();
|
|
160
|
+
if (token)
|
|
161
|
+
return { token, tokenPresent: true };
|
|
162
|
+
}
|
|
144
163
|
const candidates = agent === 'codex'
|
|
145
164
|
? [path.join(home, '.codex', 'config.toml'), path.join(home, '.codex', 'auth.json')]
|
|
146
|
-
:
|
|
165
|
+
: agent === 'claude'
|
|
166
|
+
? [path.join(home, '.claude', 'settings.json'), path.join(home, '.claude.json')]
|
|
167
|
+
: [path.join(home, '.dashscope', 'config.json')];
|
|
147
168
|
for (const file of candidates) {
|
|
148
169
|
const text = readSmallTextFile(file);
|
|
149
170
|
if (!text)
|
|
@@ -167,15 +188,15 @@ function readSmallTextFile(file) {
|
|
|
167
188
|
}
|
|
168
189
|
}
|
|
169
190
|
function extractBaseUrl(text) {
|
|
170
|
-
const match = text.match(/(?:base_url|baseURL|api_url|apiUrl|ANTHROPIC_BASE_URL|OPENAI_BASE_URL)["'\s:=]+([^"'\s,}]+)/i);
|
|
191
|
+
const match = text.match(/(?:base_url|baseURL|api_url|apiUrl|ANTHROPIC_BASE_URL|OPENAI_BASE_URL|DASHSCOPE_BASE_URL)["'\s:=]+([^"'\s,}]+)/i);
|
|
171
192
|
return match?.[1]?.trim();
|
|
172
193
|
}
|
|
173
194
|
function extractTokenLikeValue(text, agent) {
|
|
174
|
-
const prefix = agent === 'claude' ? 'sk-ant-' : 'sk-';
|
|
195
|
+
const prefix = agent === 'claude' ? 'sk-ant-' : agent === 'pi' ? 'sk-' : 'sk-';
|
|
175
196
|
const direct = text.match(new RegExp(`${prefix.replace(/-/g, '\\-')}[A-Za-z0-9_\\-]{8,}`));
|
|
176
197
|
if (direct?.[0])
|
|
177
198
|
return direct[0];
|
|
178
|
-
const keyMatch = text.match(/(?:api[_-]?key|auth[_-]?token|token)["'\s:=]+([A-Za-z0-9_\-.]{12,})/i);
|
|
199
|
+
const keyMatch = text.match(/(?:api[_-]?key|auth[_-]?token|token|dashscope)["'\s:=]+([A-Za-z0-9_\-.]{12,})/i);
|
|
179
200
|
return keyMatch?.[1]?.trim();
|
|
180
201
|
}
|
|
181
202
|
function toSummary(input) {
|
|
@@ -28,7 +28,14 @@ function managerInstructionsPath(workDir, sessionId) {
|
|
|
28
28
|
}
|
|
29
29
|
function buildStableManagerInstructions(workDir, managerSessionId) {
|
|
30
30
|
const projectInstructions = readProjectAgentsMd(workDir);
|
|
31
|
-
const
|
|
31
|
+
const managerRuntime = getManagerRuntimeService();
|
|
32
|
+
const channelInstructions = managerRuntime?.getManagerExternalChannelSystemPrompt(managerSessionId) ?? '';
|
|
33
|
+
const workerDefaults = typeof managerRuntime?.getManagerWorkerDefaults === 'function'
|
|
34
|
+
? managerRuntime.getManagerWorkerDefaults(managerSessionId)
|
|
35
|
+
: undefined;
|
|
36
|
+
const workerDefaultInstructions = workerDefaults?.agentType
|
|
37
|
+
? `默认 worker Agent:${workerDefaults.agentType}${workerDefaults.modelId ? `\n默认 worker 模型:${workerDefaults.modelId}` : ''}。除非用户明确要求或任务明显需要其他 Agent/模型,创建 worker 时优先使用这个默认组合。`
|
|
38
|
+
: '';
|
|
32
39
|
const sections = [
|
|
33
40
|
'# Shennian Manager Instructions',
|
|
34
41
|
'This file is generated by Shennian for a Manager Agent substrate session. Do not edit it by hand.',
|
|
@@ -36,6 +43,9 @@ function buildStableManagerInstructions(workDir, managerSessionId) {
|
|
|
36
43
|
? `## Project Instructions\n\n${projectInstructions}`
|
|
37
44
|
: '',
|
|
38
45
|
`## Manager Instructions\n\n${MANAGER_SYSTEM_PROMPT}`,
|
|
46
|
+
workerDefaultInstructions
|
|
47
|
+
? `## Worker Defaults\n\n${workerDefaultInstructions}`
|
|
48
|
+
: '',
|
|
39
49
|
channelInstructions
|
|
40
50
|
? `## External Message Channel Instructions\n\n${channelInstructions}`
|
|
41
51
|
: '',
|
|
@@ -7,7 +7,7 @@ type ShellCommandSpec = {
|
|
|
7
7
|
shell: 'bash' | 'powershell';
|
|
8
8
|
};
|
|
9
9
|
export declare function buildShellCommandSpec(command: string, platform?: NodeJS.Platform): ShellCommandSpec;
|
|
10
|
-
export declare function createPiModel(modelId?: string): Model<'openai-completions'>;
|
|
10
|
+
export declare function createPiModel(modelId?: string, overrides?: Partial<Pick<Model<'openai-completions'>, 'baseUrl' | 'provider' | 'compat'>>): Model<'openai-completions'>;
|
|
11
11
|
export declare const SYSTEM_PROMPT = "\u4F60\u662F\u795E\u5FF5\u5185\u7F6E\u7F16\u7A0B\u52A9\u624B\uFF0C\u8FD0\u884C\u5728\u7528\u6237\u672C\u5730\u673A\u5668\u4E0A\u3002\n\u4F60\u53EF\u4EE5\u8BFB\u5199\u6587\u4EF6\u3001\u6267\u884C shell \u547D\u4EE4\u3001\u5E2E\u52A9\u7528\u6237\u5B8C\u6210\u7F16\u7A0B\u548C\u7CFB\u7EDF\u7BA1\u7406\u4EFB\u52A1\u3002\n\u5DE5\u4F5C\u76EE\u5F55\u5DF2\u8BBE\u7F6E\uFF0C\u64CD\u4F5C\u6587\u4EF6\u65F6\u4F7F\u7528\u76F8\u5BF9\u8DEF\u5F84\u6216\u7EDD\u5BF9\u8DEF\u5F84\u5747\u53EF\u3002\n\u5F53\u524D shell \u4F1A\u968F\u64CD\u4F5C\u7CFB\u7EDF\u9009\u62E9\uFF1AWindows \u4F7F\u7528 PowerShell\uFF0CmacOS/Linux \u4F7F\u7528 bash\u3002Windows \u4E0B\u9700\u8981\u771F\u5B9E curl \u65F6\u4F7F\u7528 curl.exe\uFF0C\u907F\u514D PowerShell \u7684 curl \u522B\u540D\u3002\n\u4FDD\u6301\u56DE\u590D\u7B80\u6D01\u3001\u51C6\u786E\uFF0C\u4E2D\u6587\u56DE\u590D\u3002";
|
|
12
12
|
export declare const CONTEXT_TOKEN_THRESHOLD = 90000;
|
|
13
13
|
export declare const KEEP_RECENT_MESSAGES = 6;
|
|
@@ -17,18 +17,19 @@ export function buildShellCommandSpec(command, platform = process.platform) {
|
|
|
17
17
|
shell: 'bash',
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
export function createPiModel(modelId = PI_DEFAULT_MODEL_ID) {
|
|
20
|
+
export function createPiModel(modelId = PI_DEFAULT_MODEL_ID, overrides = {}) {
|
|
21
21
|
return {
|
|
22
22
|
id: modelId,
|
|
23
23
|
name: modelId,
|
|
24
24
|
api: 'openai-completions',
|
|
25
|
-
provider: 'shennian-proxy',
|
|
26
|
-
baseUrl: '',
|
|
25
|
+
provider: overrides.provider ?? 'shennian-proxy',
|
|
26
|
+
baseUrl: overrides.baseUrl ?? '',
|
|
27
27
|
reasoning: false,
|
|
28
28
|
input: ['text', 'image'],
|
|
29
29
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
30
30
|
contextWindow: 128000,
|
|
31
31
|
maxTokens: 8192,
|
|
32
|
+
...(overrides.compat ? { compat: overrides.compat } : {}),
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
export const SYSTEM_PROMPT = `你是神念内置编程助手,运行在用户本地机器上。
|
package/dist/src/agents/pi.d.ts
CHANGED
package/dist/src/agents/pi.js
CHANGED
|
@@ -6,14 +6,17 @@ import path from 'node:path';
|
|
|
6
6
|
import { execFile } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { Agent, streamProxy } from '@mariozechner/pi-agent-core';
|
|
9
|
+
import { streamOpenAICompletions } from '@mariozechner/pi-ai/openai-completions';
|
|
9
10
|
import { Type } from '@sinclair/typebox';
|
|
10
11
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
11
12
|
import { buildExternalChannelInstructions } from './external-channel-instructions.js';
|
|
12
13
|
import { loadConfig } from '../config/index.js';
|
|
14
|
+
import { getManagedAgentProviderConfig } from './config-status.js';
|
|
13
15
|
import { SERVERS } from '../region.js';
|
|
14
16
|
import { buildRollingSummary, buildShellCommandSpec, cloneMessages, CONTEXT_TOKEN_THRESHOLD, createPiModel, estimateTokens, getSessionDir, KEEP_RECENT_MESSAGES, LEGACY_SUMMARY_FILENAME, longestCommonPrefixLength, MESSAGES_FILENAME, messagesToText, PI_DEFAULT_MODEL_ID, requestProxySummary, SNAPSHOT_FILENAME, SYSTEM_PROMPT, SUMMARY_FILENAME, } from './pi-context.js';
|
|
15
17
|
export { buildShellCommandSpec } from './pi-context.js';
|
|
16
18
|
const execFileAsync = promisify(execFile);
|
|
19
|
+
const DASHSCOPE_COMPATIBLE_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
|
17
20
|
// ── Local tools ───────────────────────────────────────────────────────────────
|
|
18
21
|
function resolvePiToolPath(workDir, filePath) {
|
|
19
22
|
return path.resolve(workDir, filePath);
|
|
@@ -26,6 +29,7 @@ async function executePiShellCommand(workDir, command, extraEnv, signal) {
|
|
|
26
29
|
timeout: 30_000,
|
|
27
30
|
signal,
|
|
28
31
|
maxBuffer: 1024 * 1024,
|
|
32
|
+
windowsHide: true,
|
|
29
33
|
});
|
|
30
34
|
return { stdout, stderr, shell: spec.shell };
|
|
31
35
|
}
|
|
@@ -239,6 +243,7 @@ export class PiAdapter extends AgentAdapter {
|
|
|
239
243
|
terminalState = 'open';
|
|
240
244
|
authToken = null;
|
|
241
245
|
proxyUrl = null;
|
|
246
|
+
providerConfig;
|
|
242
247
|
sessionDir = null;
|
|
243
248
|
messagesPath = null;
|
|
244
249
|
snapshotPath = null;
|
|
@@ -294,6 +299,11 @@ export class PiAdapter extends AgentAdapter {
|
|
|
294
299
|
}
|
|
295
300
|
this.authToken = authToken;
|
|
296
301
|
this.proxyUrl = (config.serverUrl ?? SERVERS.cn.url).replace(/\/$/, '');
|
|
302
|
+
const managedProviderConfig = getManagedAgentProviderConfig('pi');
|
|
303
|
+
const dashscopeKey = config.apiKeys?.dashscope?.trim();
|
|
304
|
+
this.providerConfig = managedProviderConfig ?? (dashscopeKey
|
|
305
|
+
? { agent: 'pi', token: dashscopeKey, updatedAt: '' }
|
|
306
|
+
: undefined);
|
|
297
307
|
this.runId = randomUUID();
|
|
298
308
|
this.seq = 0;
|
|
299
309
|
this.emittedLengths.clear();
|
|
@@ -357,11 +367,30 @@ export class PiAdapter extends AgentAdapter {
|
|
|
357
367
|
model: createPiModel(),
|
|
358
368
|
tools,
|
|
359
369
|
},
|
|
360
|
-
streamFn: (model, context, options) =>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
370
|
+
streamFn: (model, context, options) => {
|
|
371
|
+
const providerConfig = this.providerConfig;
|
|
372
|
+
if (providerConfig?.token) {
|
|
373
|
+
return streamOpenAICompletions(createPiModel(model.id, {
|
|
374
|
+
provider: 'dashscope',
|
|
375
|
+
baseUrl: providerConfig.baseUrl || DASHSCOPE_COMPATIBLE_BASE_URL,
|
|
376
|
+
compat: {
|
|
377
|
+
supportsDeveloperRole: false,
|
|
378
|
+
supportsStore: false,
|
|
379
|
+
supportsReasoningEffort: false,
|
|
380
|
+
maxTokensField: 'max_tokens',
|
|
381
|
+
thinkingFormat: 'qwen',
|
|
382
|
+
},
|
|
383
|
+
}), context, {
|
|
384
|
+
...options,
|
|
385
|
+
apiKey: providerConfig.token,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return streamProxy(model, context, {
|
|
389
|
+
...options,
|
|
390
|
+
authToken: this.authToken,
|
|
391
|
+
proxyUrl: this.proxyUrl,
|
|
392
|
+
});
|
|
393
|
+
},
|
|
365
394
|
transformContext: (messages) => this.compressContext(messages),
|
|
366
395
|
});
|
|
367
396
|
this.agent = agent;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import type { Command } from 'commander';
|
|
2
3
|
export { buildWindowsLauncherCommand, buildWindowsScheduledTaskXml, buildWindowsStartupVbs, } from './daemon-windows.js';
|
|
3
4
|
export declare function isSafeSnapshotEnvKey(key: string): boolean;
|
|
@@ -23,6 +24,11 @@ export type ServiceLaunchSpec = {
|
|
|
23
24
|
args: string[];
|
|
24
25
|
mode: ServiceLaunchMode;
|
|
25
26
|
};
|
|
27
|
+
export type DetachedLaunchSpec = {
|
|
28
|
+
command: string;
|
|
29
|
+
args: string[];
|
|
30
|
+
windowsVerbatimArguments?: boolean;
|
|
31
|
+
};
|
|
26
32
|
export declare function isEphemeralCliPath(candidate: string): boolean;
|
|
27
33
|
export declare function resolveServiceLaunchSpec(input: {
|
|
28
34
|
nodeExec: string;
|
|
@@ -39,6 +45,7 @@ export declare function recordStartedDaemon(childPid: number | undefined): void;
|
|
|
39
45
|
export declare function getDaemonStatus(opts?: {
|
|
40
46
|
cleanupStale?: boolean;
|
|
41
47
|
}): DaemonStatus;
|
|
48
|
+
export declare function buildDaemonSpawnOptions(launch: DetachedLaunchSpec, logFd: number, env?: NodeJS.ProcessEnv): Parameters<typeof spawn>[2];
|
|
42
49
|
export declare function captureEnvForService(): Record<string, string>;
|
|
43
50
|
/**
|
|
44
51
|
* Save current env vars so the daemon can load them at startup.
|
|
@@ -160,6 +160,7 @@ function inferDaemonLauncherFromProcess(pid) {
|
|
|
160
160
|
encoding: 'utf-8',
|
|
161
161
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
162
162
|
timeout: 1000,
|
|
163
|
+
windowsHide: true,
|
|
163
164
|
}).replace(/\\/g, '/');
|
|
164
165
|
if (command.includes('/node_modules/shennian/') ||
|
|
165
166
|
command.includes('/bin/shennian') ||
|
|
@@ -229,6 +230,7 @@ function findCommandPath(binary) {
|
|
|
229
230
|
const output = execSync(command, {
|
|
230
231
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
231
232
|
encoding: 'utf-8',
|
|
233
|
+
windowsHide: true,
|
|
232
234
|
});
|
|
233
235
|
const first = output
|
|
234
236
|
.split(/\r?\n/)
|
|
@@ -257,7 +259,7 @@ function resolveCurrentServiceLaunchSpec() {
|
|
|
257
259
|
}
|
|
258
260
|
function removeLegacyWindowsTask() {
|
|
259
261
|
try {
|
|
260
|
-
execSync(`schtasks /delete /tn "${WINDOWS_TASK_NAME}" /f`, { stdio: 'pipe' });
|
|
262
|
+
execSync(`schtasks /delete /tn "${WINDOWS_TASK_NAME}" /f`, { stdio: 'pipe', windowsHide: true });
|
|
261
263
|
}
|
|
262
264
|
catch {
|
|
263
265
|
// Ignore missing legacy Task Scheduler entries during migration.
|
|
@@ -295,6 +297,15 @@ function buildDetachedLaunchSpec(spec) {
|
|
|
295
297
|
args: spec.args,
|
|
296
298
|
};
|
|
297
299
|
}
|
|
300
|
+
export function buildDaemonSpawnOptions(launch, logFd, env = process.env) {
|
|
301
|
+
return {
|
|
302
|
+
detached: true,
|
|
303
|
+
stdio: ['ignore', logFd, logFd],
|
|
304
|
+
env,
|
|
305
|
+
windowsHide: true,
|
|
306
|
+
...(launch.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
298
309
|
function installWindowsScheduledTask() {
|
|
299
310
|
const launch = resolveCurrentServiceLaunchSpec();
|
|
300
311
|
fs.writeFileSync(WINDOWS_STARTUP_CMD, buildWindowsLauncherCommand(launch, LOG_FILE));
|
|
@@ -310,8 +321,9 @@ function installWindowsScheduledTask() {
|
|
|
310
321
|
try {
|
|
311
322
|
execSync(`schtasks /create /tn "${WINDOWS_TASK_NAME}" /xml "${WINDOWS_TASK_XML}" /f`, {
|
|
312
323
|
stdio: 'pipe',
|
|
324
|
+
windowsHide: true,
|
|
313
325
|
});
|
|
314
|
-
execSync(`schtasks /run /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe' });
|
|
326
|
+
execSync(`schtasks /run /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe', windowsHide: true });
|
|
315
327
|
return true;
|
|
316
328
|
}
|
|
317
329
|
catch {
|
|
@@ -423,12 +435,7 @@ export function startDaemonProcess(opts = {}) {
|
|
|
423
435
|
}
|
|
424
436
|
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
425
437
|
const launch = buildDetachedLaunchSpec(resolveCurrentServiceLaunchSpec());
|
|
426
|
-
const child = spawn(launch.command, launch.args,
|
|
427
|
-
detached: true,
|
|
428
|
-
stdio: ['ignore', logFd, logFd],
|
|
429
|
-
env: process.env,
|
|
430
|
-
...(launch.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
431
|
-
});
|
|
438
|
+
const child = spawn(launch.command, launch.args, buildDaemonSpawnOptions(launch, logFd));
|
|
432
439
|
child.unref();
|
|
433
440
|
fs.closeSync(logFd);
|
|
434
441
|
recordStartedDaemon(child.pid);
|
|
@@ -456,12 +463,12 @@ export function installService() {
|
|
|
456
463
|
fs.writeFileSync(LAUNCHD_PLIST, buildPlist());
|
|
457
464
|
try {
|
|
458
465
|
// Unload first (ignore errors), then reload - this starts the service immediately
|
|
459
|
-
execSync(`launchctl unload "${LAUNCHD_PLIST}" 2>/dev/null; launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
466
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST}" 2>/dev/null; launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
460
467
|
return true; // launchd started it; caller must NOT also call startDaemonProcess
|
|
461
468
|
}
|
|
462
469
|
catch {
|
|
463
470
|
try {
|
|
464
|
-
execSync(`launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
471
|
+
execSync(`launchctl load -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
465
472
|
return true;
|
|
466
473
|
}
|
|
467
474
|
catch {
|
|
@@ -477,6 +484,7 @@ export function installService() {
|
|
|
477
484
|
try {
|
|
478
485
|
execSync('systemctl --user daemon-reload && systemctl --user enable shennian', {
|
|
479
486
|
stdio: 'pipe',
|
|
487
|
+
windowsHide: true,
|
|
480
488
|
});
|
|
481
489
|
}
|
|
482
490
|
catch {
|
|
@@ -486,13 +494,13 @@ export function installService() {
|
|
|
486
494
|
// Enable linger so the user systemd session (and thus this service) persists
|
|
487
495
|
// across reboots even without an active login session.
|
|
488
496
|
try {
|
|
489
|
-
execSync(`loginctl enable-linger ${os.userInfo().username}`, { stdio: 'pipe' });
|
|
497
|
+
execSync(`loginctl enable-linger ${os.userInfo().username}`, { stdio: 'pipe', windowsHide: true });
|
|
490
498
|
}
|
|
491
499
|
catch {
|
|
492
500
|
// loginctl is unavailable on some distros/containers; auto-start still works after login.
|
|
493
501
|
}
|
|
494
502
|
try {
|
|
495
|
-
execSync('systemctl --user restart shennian', { stdio: 'pipe' });
|
|
503
|
+
execSync('systemctl --user restart shennian', { stdio: 'pipe', windowsHide: true });
|
|
496
504
|
return true;
|
|
497
505
|
}
|
|
498
506
|
catch {
|
|
@@ -572,7 +580,7 @@ async function disableRemoteAccess(opts = {}) {
|
|
|
572
580
|
case 'darwin': {
|
|
573
581
|
if (fs.existsSync(LAUNCHD_PLIST)) {
|
|
574
582
|
try {
|
|
575
|
-
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
583
|
+
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
576
584
|
}
|
|
577
585
|
catch {
|
|
578
586
|
// The job may already be unloaded; keep the plist for future re-enable.
|
|
@@ -582,7 +590,7 @@ async function disableRemoteAccess(opts = {}) {
|
|
|
582
590
|
}
|
|
583
591
|
case 'linux': {
|
|
584
592
|
try {
|
|
585
|
-
execSync('systemctl --user disable --now shennian', { stdio: 'pipe' });
|
|
593
|
+
execSync('systemctl --user disable --now shennian', { stdio: 'pipe', windowsHide: true });
|
|
586
594
|
}
|
|
587
595
|
catch {
|
|
588
596
|
// systemd user services may be unavailable; still stop the manual daemon.
|
|
@@ -591,13 +599,13 @@ async function disableRemoteAccess(opts = {}) {
|
|
|
591
599
|
}
|
|
592
600
|
case 'win32': {
|
|
593
601
|
try {
|
|
594
|
-
execSync(`schtasks /change /tn "${WINDOWS_TASK_NAME}" /disable`, { stdio: 'pipe' });
|
|
602
|
+
execSync(`schtasks /change /tn "${WINDOWS_TASK_NAME}" /disable`, { stdio: 'pipe', windowsHide: true });
|
|
595
603
|
}
|
|
596
604
|
catch {
|
|
597
605
|
// Task may not exist yet; still stop the manual daemon.
|
|
598
606
|
}
|
|
599
607
|
try {
|
|
600
|
-
execSync(`schtasks /end /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe' });
|
|
608
|
+
execSync(`schtasks /end /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe', windowsHide: true });
|
|
601
609
|
}
|
|
602
610
|
catch {
|
|
603
611
|
// Task may not be running.
|
|
@@ -722,7 +730,7 @@ function daemonLogs(opts) {
|
|
|
722
730
|
try {
|
|
723
731
|
const out = execSync(os.platform() === 'win32'
|
|
724
732
|
? `powershell Get-Content -Tail ${opts.lines} "${LOG_FILE}"`
|
|
725
|
-
: `tail -n ${opts.lines} "${LOG_FILE}"`, { encoding: 'utf-8' });
|
|
733
|
+
: `tail -n ${opts.lines} "${LOG_FILE}"`, { encoding: 'utf-8', windowsHide: true });
|
|
726
734
|
process.stdout.write(out);
|
|
727
735
|
}
|
|
728
736
|
catch {
|
|
@@ -737,7 +745,7 @@ async function daemonUninstall() {
|
|
|
737
745
|
case 'darwin': {
|
|
738
746
|
if (fs.existsSync(LAUNCHD_PLIST)) {
|
|
739
747
|
try {
|
|
740
|
-
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe' });
|
|
748
|
+
execSync(`launchctl unload -w "${LAUNCHD_PLIST}"`, { stdio: 'pipe', windowsHide: true });
|
|
741
749
|
}
|
|
742
750
|
catch {
|
|
743
751
|
// Continue removing the plist even if launchctl already forgot about it.
|
|
@@ -749,7 +757,7 @@ async function daemonUninstall() {
|
|
|
749
757
|
}
|
|
750
758
|
case 'linux': {
|
|
751
759
|
try {
|
|
752
|
-
execSync('systemctl --user disable --now shennian', { stdio: 'pipe' });
|
|
760
|
+
execSync('systemctl --user disable --now shennian', { stdio: 'pipe', windowsHide: true });
|
|
753
761
|
}
|
|
754
762
|
catch {
|
|
755
763
|
// Service may already be absent or systemd user services may be unavailable.
|
|
@@ -757,7 +765,7 @@ async function daemonUninstall() {
|
|
|
757
765
|
if (fs.existsSync(SYSTEMD_UNIT)) {
|
|
758
766
|
fs.unlinkSync(SYSTEMD_UNIT);
|
|
759
767
|
try {
|
|
760
|
-
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
768
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe', windowsHide: true });
|
|
761
769
|
}
|
|
762
770
|
catch {
|
|
763
771
|
// Ignore reload errors during cleanup.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ExternalAttachmentKind = 'image' | 'video' | 'file';
|
|
2
|
+
export type ExternalAttachmentPayload = {
|
|
3
|
+
kind: ExternalAttachmentKind;
|
|
4
|
+
name: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
size: number;
|
|
7
|
+
dataBase64: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function readExternalAttachment(filePath: string, kind: ExternalAttachmentKind): ExternalAttachmentPayload;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @arch docs/features/wecom-managed-channel.md
|
|
2
|
+
// @test src/__tests__/external-command.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const MIME_BY_EXT = {
|
|
6
|
+
'.jpg': 'image/jpeg',
|
|
7
|
+
'.jpeg': 'image/jpeg',
|
|
8
|
+
'.png': 'image/png',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.webp': 'image/webp',
|
|
11
|
+
'.mp4': 'video/mp4',
|
|
12
|
+
'.mov': 'video/quicktime',
|
|
13
|
+
'.pdf': 'application/pdf',
|
|
14
|
+
'.txt': 'text/plain',
|
|
15
|
+
'.md': 'text/markdown',
|
|
16
|
+
'.csv': 'text/csv',
|
|
17
|
+
'.doc': 'application/msword',
|
|
18
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
19
|
+
'.xls': 'application/vnd.ms-excel',
|
|
20
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
21
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
22
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
23
|
+
'.zip': 'application/zip',
|
|
24
|
+
};
|
|
25
|
+
const MAX_EXTERNAL_ATTACHMENT_BYTES = Number(process.env.SHENNIAN_EXTERNAL_ATTACHMENT_MAX_BYTES || 50 * 1024 * 1024);
|
|
26
|
+
function inferMimeType(filePath, kind) {
|
|
27
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
28
|
+
if (MIME_BY_EXT[ext])
|
|
29
|
+
return MIME_BY_EXT[ext];
|
|
30
|
+
if (kind === 'image')
|
|
31
|
+
return 'image/jpeg';
|
|
32
|
+
if (kind === 'video')
|
|
33
|
+
return 'video/mp4';
|
|
34
|
+
return 'application/octet-stream';
|
|
35
|
+
}
|
|
36
|
+
export function readExternalAttachment(filePath, kind) {
|
|
37
|
+
const absolutePath = path.resolve(filePath);
|
|
38
|
+
const stat = fs.statSync(absolutePath);
|
|
39
|
+
if (!stat.isFile())
|
|
40
|
+
throw new Error(`Attachment is not a file: ${absolutePath}`);
|
|
41
|
+
if (stat.size > MAX_EXTERNAL_ATTACHMENT_BYTES) {
|
|
42
|
+
throw new Error(`Attachment is too large: ${stat.size} bytes. Max: ${MAX_EXTERNAL_ATTACHMENT_BYTES} bytes.`);
|
|
43
|
+
}
|
|
44
|
+
const buffer = fs.readFileSync(absolutePath);
|
|
45
|
+
return {
|
|
46
|
+
kind,
|
|
47
|
+
name: path.basename(absolutePath),
|
|
48
|
+
mimeType: inferMimeType(absolutePath, kind),
|
|
49
|
+
size: buffer.byteLength,
|
|
50
|
+
dataBase64: buffer.toString('base64'),
|
|
51
|
+
};
|
|
52
|
+
}
|