@zhin.js/runtime 1.1.9 → 1.1.11

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.
@@ -2071,6 +2071,45 @@ export declare const HOST_CONFIG_SCHEMA: {
2071
2071
  type: string;
2072
2072
  description: string;
2073
2073
  };
2074
+ ambient: {
2075
+ type: string;
2076
+ additionalProperties: boolean;
2077
+ description: string;
2078
+ properties: {
2079
+ conversations: {
2080
+ type: string;
2081
+ items: {
2082
+ type: string;
2083
+ };
2084
+ description: string;
2085
+ };
2086
+ judge: {
2087
+ type: string;
2088
+ description: string;
2089
+ };
2090
+ quietMs: {
2091
+ type: string[];
2092
+ description: string;
2093
+ };
2094
+ cooldownMs: {
2095
+ type: string[];
2096
+ description: string;
2097
+ };
2098
+ maxPerHour: {
2099
+ type: string[];
2100
+ description: string;
2101
+ };
2102
+ minConfidence: {
2103
+ type: string[];
2104
+ description: string;
2105
+ };
2106
+ mode: {
2107
+ type: string;
2108
+ enum: string[];
2109
+ description: string;
2110
+ };
2111
+ };
2112
+ };
2074
2113
  };
2075
2114
  description: string;
2076
2115
  };
@@ -14,6 +14,8 @@ export const HOST_CONFIG_SCHEMA = deepFreeze(hostConfigSchema);
14
14
  /** Host configuration keys consumed by Runtime composition and Host installers. */
15
15
  export const HOST_CONFIG_KEYS = Object.freeze(Object.keys(HOST_CONFIG_SCHEMA.properties));
16
16
  const ROOT_FEATURE_CONFIG_KEYS = ['menuKeyword', 'menuStyle'];
17
+ /** Command routing keys the framework injects into every Plugin config schema. */
18
+ const COMMAND_CONFIG_KEYS = ['commandNamespace', 'disabledCommands'];
17
19
  export class ConfigSchemaCollisionError extends Error {
18
20
  plugin;
19
21
  instanceKey;
@@ -214,7 +216,7 @@ async function readOwnSchema(node, modules, validatedSchemas) {
214
216
  throw new TypeError(`${file} root schema must declare properties; composition keywords (anyOf/oneOf/allOf/$ref) are not supported`);
215
217
  }
216
218
  const properties = schemaProperties(schema);
217
- for (const key of ['commandNamespace', ...(!node.parent ? ROOT_FEATURE_CONFIG_KEYS : [])]) {
219
+ for (const key of [...COMMAND_CONFIG_KEYS, ...(!node.parent ? ROOT_FEATURE_CONFIG_KEYS : [])]) {
218
220
  if (Object.hasOwn(properties, key))
219
221
  throw new ConfigSchemaCollisionError(node.id, key);
220
222
  }
@@ -231,6 +233,13 @@ async function readOwnSchema(node, modules, validatedSchemas) {
231
233
  description: 'Explicit command namespace prepended after the Endpoint commandPrefix.',
232
234
  'x-descriptionZh': '显式指令命名空间;在 Endpoint commandPrefix 之后参与路由。',
233
235
  },
236
+ disabledCommands: {
237
+ type: 'array',
238
+ uniqueItems: true,
239
+ items: { type: 'string', minLength: 1, pattern: '^\\S+(?: \\S+)*$' },
240
+ description: 'Commands of this Plugin instance to leave out of routing, by local path without commandNamespace or parameters (e.g. "fanyi", "netease login"). A parent path also disables its subcommands.',
241
+ 'x-descriptionZh': '不注册的指令:写插件内的指令路径(不含 commandNamespace 和参数,如 fanyi、netease login),写上级路径会连同子命令一起禁用。',
242
+ },
234
243
  ...properties,
235
244
  },
236
245
  });
@@ -0,0 +1,12 @@
1
+ interface EditorTempPattern {
2
+ readonly editor: string;
3
+ readonly test: (name: string) => boolean;
4
+ }
5
+ /** Ordered, documented list; each entry is covered by a unit test. */
6
+ export declare const EDITOR_TEMP_FILE_PATTERNS: readonly EditorTempPattern[];
7
+ /**
8
+ * Returns true when `source` (a path or a bare file name) is an editor temp or
9
+ * backup file. Only the basename is inspected.
10
+ */
11
+ export declare function isEditorTempFile(source: string): boolean;
12
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Editor temp, swap, lock, and backup files that must never trigger an HMR
3
+ * reload (#857 P2-11).
4
+ *
5
+ * Editors write these next to the real source while saving. Several of them
6
+ * keep the source extension (for example `.!97910!index.ts`), so an
7
+ * extension allow-list alone is not enough.
8
+ *
9
+ * Matching uses the basename only, so a directory such as `tmp/` or a file
10
+ * such as `tmp.ts` or `foo.tmp.ts` is still watched: those are ordinary
11
+ * sources a plugin may really import. Only the exact shapes below are skipped.
12
+ */
13
+ import { basename } from 'node:path';
14
+ /** Ordered, documented list; each entry is covered by a unit test. */
15
+ export const EDITOR_TEMP_FILE_PATTERNS = Object.freeze([
16
+ // `.!<digits>!<name>`: atomic-save temp file seen in practice (`.!97910!index.ts`).
17
+ { editor: 'atomic save (.!<pid>!name)', test: (name) => /^\.!\d+!/.test(name) },
18
+ // Vim swap files: `.index.ts.swp`, then `.swo`, `.swx` when the first one is taken.
19
+ { editor: 'vim swap (*.swp, *.swo, *.swx)', test: (name) => /\.sw[pox]$/.test(name) },
20
+ // Vim `writebackup` probe: Vim creates and deletes `4913` to test the directory.
21
+ { editor: 'vim probe (4913)', test: (name) => name === '4913' },
22
+ // Backup copies from Vim, Emacs, nano and others: `index.ts~`.
23
+ { editor: 'backup (*~)', test: (name) => name.endsWith('~') },
24
+ // Emacs lock files: `.#index.ts` (usually a dangling symlink).
25
+ { editor: 'emacs lock (.#*)', test: (name) => name.startsWith('.#') },
26
+ // Generic temp files: `index.ts.tmp`. `foo.tmp.ts` is a real source and still reloads.
27
+ { editor: 'temp (*.tmp)', test: (name) => name.endsWith('.tmp') },
28
+ // JetBrains "safe write": `index.ts___jb_tmp___` and `index.ts___jb_old___`.
29
+ {
30
+ editor: 'JetBrains safe write (*___jb_tmp___, *___jb_old___)',
31
+ test: (name) => name.endsWith('___jb_tmp___') || name.endsWith('___jb_old___'),
32
+ },
33
+ ]);
34
+ /**
35
+ * Returns true when `source` (a path or a bare file name) is an editor temp or
36
+ * backup file. Only the basename is inspected.
37
+ */
38
+ export function isEditorTempFile(source) {
39
+ // Accept both separators so Windows paths behave the same on every host.
40
+ const name = basename(source.replaceAll('\\', '/'));
41
+ if (!name)
42
+ return false;
43
+ return EDITOR_TEMP_FILE_PATTERNS.some((pattern) => pattern.test(name));
44
+ }
@@ -1,5 +1,6 @@
1
1
  import { normalizeSourcePath } from './source-path.js';
2
2
  import { InvalidationPlanner, } from './invalidation-planner.js';
3
+ import { isEditorTempFile } from './editor-temp-file.js';
3
4
  export class HmrCoordinator {
4
5
  options;
5
6
  #pending = new Set();
@@ -24,6 +25,9 @@ export class HmrCoordinator {
24
25
  }
25
26
  this.#syncModuleState();
26
27
  this.#unwatch = this.options.modules.watch((source) => {
28
+ // Any ModuleRuntime watcher may report editor temp/backup files; they never reload (#857).
29
+ if (isEditorTempFile(source))
30
+ return;
27
31
  void this.enqueue(source).catch(() => undefined);
28
32
  });
29
33
  return () => this.stop();
@@ -2855,6 +2855,60 @@
2855
2855
  "errorTemplate": {
2856
2856
  "type": "string",
2857
2857
  "description": "错误提示模板(默认 '❌ AI 处理失败: {error}')"
2858
+ },
2859
+ "ambient": {
2860
+ "type": "object",
2861
+ "additionalProperties": true,
2862
+ "description": "群聊旁听:群里没人叫机器人时,由判断模型决定是否接话。被 @、引用和私聊不经过这里,总会交给 Agent(Agent 也可以选择不回复)。",
2863
+ "properties": {
2864
+ "conversations": {
2865
+ "type": "array",
2866
+ "items": {
2867
+ "type": "string"
2868
+ },
2869
+ "description": "开启旁听的群,格式 platform:groupId;* 表示所有群。默认为空(关闭)"
2870
+ },
2871
+ "judge": {
2872
+ "type": "string",
2873
+ "description": "判断模型 providerAlias/model;默认用主 Agent 的模型"
2874
+ },
2875
+ "quietMs": {
2876
+ "type": [
2877
+ "number",
2878
+ "string"
2879
+ ],
2880
+ "description": "群安静多久后才判断一次(毫秒,默认 4000)"
2881
+ },
2882
+ "cooldownMs": {
2883
+ "type": [
2884
+ "number",
2885
+ "string"
2886
+ ],
2887
+ "description": "机器人在群里说话后的冷却时间(毫秒,默认 120000)"
2888
+ },
2889
+ "maxPerHour": {
2890
+ "type": [
2891
+ "number",
2892
+ "string"
2893
+ ],
2894
+ "description": "每个群每小时最多主动接话次数(默认 3)"
2895
+ },
2896
+ "minConfidence": {
2897
+ "type": [
2898
+ "number",
2899
+ "string"
2900
+ ],
2901
+ "description": "判断模型置信度门槛 0..1(默认 0.7)"
2902
+ },
2903
+ "mode": {
2904
+ "type": "string",
2905
+ "enum": [
2906
+ "active",
2907
+ "shadow"
2908
+ ],
2909
+ "description": "shadow 只记录判断结果、不回复(默认 active)"
2910
+ }
2911
+ }
2858
2912
  }
2859
2913
  },
2860
2914
  "description": "AI 触发配置"
package/lib/manifest.d.ts CHANGED
@@ -53,3 +53,13 @@ export declare class ManifestValidationError extends Error {
53
53
  constructor(issues: readonly string[]);
54
54
  }
55
55
  export declare function parsePackageJson(value: unknown, source: string): PackageJson;
56
+ /** Conditions tried, in order, when `exports["./zhin"]` is a condition object. */
57
+ export declare const ZHIN_EXPORT_CONDITIONS: readonly ["zhin", "import", "default"];
58
+ /**
59
+ * The runtime entry declared by `exports["./zhin"]`, if any.
60
+ *
61
+ * A string target is used as is; a condition object is searched for `zhin`, then `import`,
62
+ * then `default` (nested condition objects are searched the same way); an array uses its
63
+ * first target that resolves. Other conditions such as `types` or `require` are ignored.
64
+ */
65
+ export declare function zhinExportTarget(packageExports: unknown): string | undefined;
package/lib/manifest.js CHANGED
@@ -12,7 +12,7 @@ export function parsePackageJson(value, source) {
12
12
  const name = readString(record, 'name', source, issues);
13
13
  if (name && !isPackageName(name))
14
14
  issues.push(`${source}#name is not a valid package name`);
15
- const zhin = parseZhinManifest(record.zhin, `${source}#zhin`, issues);
15
+ const zhin = parseZhinManifest(record.zhin, `${source}#zhin`, issues, record.exports, `${source}#exports`);
16
16
  const version = optionalString(record.version, `${source}#version`, issues);
17
17
  const moduleType = optionalModuleType(record.type, `${source}#type`, issues);
18
18
  const main = optionalString(record.main, `${source}#main`, issues);
@@ -42,12 +42,54 @@ export function parsePackageJson(value, source) {
42
42
  zhin,
43
43
  });
44
44
  }
45
- function parseZhinManifest(value, source, issues) {
45
+ /** Conditions tried, in order, when `exports["./zhin"]` is a condition object. */
46
+ export const ZHIN_EXPORT_CONDITIONS = Object.freeze(['zhin', 'import', 'default']);
47
+ /**
48
+ * The runtime entry declared by `exports["./zhin"]`, if any.
49
+ *
50
+ * A string target is used as is; a condition object is searched for `zhin`, then `import`,
51
+ * then `default` (nested condition objects are searched the same way); an array uses its
52
+ * first target that resolves. Other conditions such as `types` or `require` are ignored.
53
+ */
54
+ export function zhinExportTarget(packageExports) {
55
+ if (!packageExports || typeof packageExports !== 'object' || Array.isArray(packageExports))
56
+ return undefined;
57
+ if (!Object.hasOwn(packageExports, './zhin'))
58
+ return undefined;
59
+ return selectExportTarget(packageExports['./zhin']);
60
+ }
61
+ function selectExportTarget(value) {
62
+ if (typeof value === 'string')
63
+ return value;
64
+ if (Array.isArray(value)) {
65
+ for (const item of value) {
66
+ const target = selectExportTarget(item);
67
+ if (target !== undefined)
68
+ return target;
69
+ }
70
+ return undefined;
71
+ }
72
+ if (!value || typeof value !== 'object')
73
+ return undefined;
74
+ for (const condition of ZHIN_EXPORT_CONDITIONS) {
75
+ if (!Object.hasOwn(value, condition))
76
+ continue;
77
+ const target = selectExportTarget(value[condition]);
78
+ if (target !== undefined)
79
+ return target;
80
+ }
81
+ return undefined;
82
+ }
83
+ function parseZhinManifest(value, source, issues, packageExports, exportsSource = 'exports') {
46
84
  const record = asRecord(value, source, issues);
47
85
  if (record.protocol !== 1)
48
86
  issues.push(`${source}.protocol must be 1`);
49
87
  const type = readString(record, 'type', source, issues);
50
- const entry = readRelativeEntry(record.entry, `${source}.entry`, issues);
88
+ // `zhin.entry` wins when present; `exports["./zhin"]` is the standard-field alternative.
89
+ const exportedEntry = record.entry === undefined ? zhinExportTarget(packageExports) : undefined;
90
+ const entry = exportedEntry !== undefined
91
+ ? readRelativeEntry(exportedEntry, `${exportsSource}["./zhin"]`, issues)
92
+ : readRelativeEntry(record.entry, `${source}.entry`, issues, `${exportsSource}["./zhin"]`);
51
93
  const engine = optionalString(record.engine, `${source}.engine`, issues);
52
94
  if (!entry || (type !== 'plugin' && type !== 'feature')) {
53
95
  if (type && type !== 'plugin' && type !== 'feature') {
@@ -132,7 +174,11 @@ function parseReferences(value, source, issues, child) {
132
174
  }];
133
175
  });
134
176
  }
135
- function readRelativeEntry(value, source, issues) {
177
+ function readRelativeEntry(value, source, issues, alternative) {
178
+ if (value === undefined && alternative) {
179
+ issues.push(`${source} or ${alternative} must declare a package-relative path starting with ./`);
180
+ return undefined;
181
+ }
136
182
  if (typeof value !== 'string' || !value.startsWith('./')) {
137
183
  issues.push(`${source} must be a package-relative path starting with ./`);
138
184
  return undefined;
@@ -4,6 +4,7 @@ import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { ModuleDependencyIndex, } from './module-dependency-index.js';
6
6
  import { withToolPackageInstallHint } from './missing-tool-package.js';
7
+ import { isEditorTempFile } from './editor-temp-file.js';
7
8
  import { ensureTypeScriptSpecifierRemap } from './typescript-specifier-remap.js';
8
9
  const ignoredDirectories = new Set([
9
10
  '.git', '.zhin', 'coverage', 'data', 'dist', 'lib', 'node_modules',
@@ -353,8 +354,12 @@ function sourceSnapshot(roots) {
353
354
  visit(root);
354
355
  return result;
355
356
  }
357
+ /** Shared by the native watcher and the polling fallback, so both skip the same files. */
356
358
  function isWatchedSource(source) {
357
359
  const name = source.slice(source.lastIndexOf(sep) + 1);
360
+ // Editor temp/backup files (e.g. `.!97910!index.ts`, `.env.swp`) never reload (#857).
361
+ if (isEditorTempFile(name))
362
+ return false;
358
363
  return watchedExtensions.has(extname(source)) || name.startsWith('.env');
359
364
  }
360
365
  /** Mirrors sourceSnapshot: any path segment matching an ignored directory opts out. */
@@ -4,3 +4,13 @@ import type { ResolvedPackage } from './package-resolver.js';
4
4
  export declare function readPluginConfigSchema(pkg: ResolvedPackage, modules?: Pick<ModuleRuntime, 'load'>): Promise<Readonly<Record<string, unknown>> | null>;
5
5
  /** Detached declaration metadata for preflight and custom-resolver startup views. */
6
6
  export declare function immutableConfigSchema(schema: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>>;
7
+ /**
8
+ * Thrown when a trusted plugin declares both `definePlugin({ config })` and a root `schema.json`.
9
+ * Neither source wins: startup stops so the two schemas cannot silently drift apart.
10
+ */
11
+ export declare class PluginConfigSourceConflictError extends TypeError {
12
+ readonly packageName: string;
13
+ readonly entryFile: string;
14
+ readonly schemaFile: string;
15
+ constructor(packageName: string, entryFile: string, schemaFile: string);
16
+ }
@@ -14,12 +14,14 @@ export async function readPluginConfigSchema(pkg, modules) {
14
14
  // Isolated plugin code never executes in the composition process.
15
15
  const manifest = pkg.packageJson.zhin;
16
16
  let declaration;
17
+ let entryFile;
17
18
  if (modules && manifest.type === 'plugin' && manifest.runtime !== 'isolated') {
18
- const module = await modules.load(resolve(pkg.root, manifest.entry));
19
+ entryFile = resolve(pkg.root, manifest.entry);
20
+ const module = await modules.load(entryFile);
19
21
  declaration = module.default?.config;
20
22
  }
21
23
  if (declaration && fileSchema !== undefined) {
22
- throw new TypeError(`${pkg.name} declares both plugin config and schema.json; choose one configuration schema`);
24
+ throw new PluginConfigSourceConflictError(pkg.name, entryFile ?? manifest.entry, file);
23
25
  }
24
26
  const schema = declaration ? declaration.toJsonSchema() : fileSchema;
25
27
  if (schema === undefined)
@@ -42,3 +44,24 @@ export function immutableConfigSchema(schema) {
42
44
  freeze(copy);
43
45
  return copy;
44
46
  }
47
+ /**
48
+ * Thrown when a trusted plugin declares both `definePlugin({ config })` and a root `schema.json`.
49
+ * Neither source wins: startup stops so the two schemas cannot silently drift apart.
50
+ */
51
+ export class PluginConfigSourceConflictError extends TypeError {
52
+ packageName;
53
+ entryFile;
54
+ schemaFile;
55
+ constructor(packageName, entryFile, schemaFile) {
56
+ super(`${packageName} declares both plugin config and schema.json; choose one configuration schema. `
57
+ + `Inline config: definePlugin({ config }) in ${entryFile}. JSON Schema: ${schemaFile}. `
58
+ + 'Zhin does not pick one for you. To keep the typed inline Schema, delete schema.json; '
59
+ + 'to keep schema.json (required for isolated plugins, readable without running the plugin), '
60
+ + 'remove config from definePlugin. '
61
+ + 'See https://zhin.js.org/authoring/define-plugin#choose-a-config-source');
62
+ this.packageName = packageName;
63
+ this.entryFile = entryFile;
64
+ this.schemaFile = schemaFile;
65
+ this.name = 'PluginConfigSourceConflictError';
66
+ }
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/runtime",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "Static Plugin graph, generation transaction and HMR Root runtime for Zhin.js",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,25 +19,25 @@
19
19
  "es-module-lexer": "2.3.1",
20
20
  "import-meta-resolve": "4.2.0",
21
21
  "semver": "7.8.5",
22
- "@zhin.js/feature-kit": "1.1.6",
22
+ "@zhin.js/feature-kit": "1.1.7",
23
23
  "@zhin.js/plugin-runtime": "1.1.15"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^26.1.2",
27
27
  "tsx": "^4.23.1",
28
28
  "typescript": "^6.0.3",
29
- "@zhin.js/adapter": "1.1.20",
30
- "@zhin.js/command": "1.1.8",
31
- "@zhin.js/component": "1.1.7",
32
- "@zhin.js/handler": "1.1.6",
33
- "@zhin.js/mcp-feature": "1.1.7",
34
- "@zhin.js/agent-feature": "1.1.7",
35
- "@zhin.js/page": "1.1.7",
36
- "@zhin.js/prompt-section": "1.1.6",
37
- "@zhin.js/skill": "1.1.7",
38
- "@zhin.js/tool": "1.1.7",
39
- "@zhin.js/middleware": "1.1.7",
40
- "@zhin.js/layout": "1.1.7"
29
+ "@zhin.js/adapter": "1.1.21",
30
+ "@zhin.js/agent-feature": "1.1.8",
31
+ "@zhin.js/command": "1.1.9",
32
+ "@zhin.js/handler": "1.1.7",
33
+ "@zhin.js/component": "1.1.8",
34
+ "@zhin.js/layout": "1.1.8",
35
+ "@zhin.js/mcp-feature": "1.1.8",
36
+ "@zhin.js/page": "1.1.8",
37
+ "@zhin.js/prompt-section": "1.1.7",
38
+ "@zhin.js/skill": "1.1.8",
39
+ "@zhin.js/middleware": "1.1.8",
40
+ "@zhin.js/tool": "1.1.8"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",