@zhin.js/plugin-runtime 1.1.3 → 1.1.5

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.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Outbound template context — conversation + incoming message metadata
3
+ * available as top-level variables inside `${expr}` template expressions.
4
+ *
5
+ * Templates can reference e.g. `${sender.name}`, `${adapter}`,
6
+ * `${content}`, `${messageId}`, `${timestamp}`.
7
+ */
8
+ export interface TemplateContext {
9
+ /** Adapter feature name (e.g. 'qq', 'sandbox'). */
10
+ readonly adapter: string;
11
+ /** Endpoint instance id. */
12
+ readonly endpoint: string;
13
+ /** Conversation kind: 'private' | 'group' | 'channel'. */
14
+ readonly kind: string;
15
+ /** Native conversation id. */
16
+ readonly conversationId: string;
17
+ /** Sender info (absent for scheduled / cross-channel sends). */
18
+ readonly sender?: Readonly<{
19
+ readonly id: string;
20
+ readonly name?: string;
21
+ readonly roles?: readonly string[];
22
+ }>;
23
+ /** Incoming message text content. */
24
+ readonly content?: string;
25
+ /** Structured message segments (images, mentions, quotes, etc.). */
26
+ readonly segments?: readonly Readonly<{
27
+ type: string;
28
+ data?: Record<string, unknown>;
29
+ }>[];
30
+ /** Platform native message id. */
31
+ readonly messageId?: string;
32
+ /** Message arrival timestamp in ms. */
33
+ readonly timestamp?: number;
34
+ /** Endpoint instance name (e.g. ICQQ uin, sandbox bot name). */
35
+ readonly endpointId?: string;
36
+ /** Whether the bot was @mentioned. */
37
+ readonly mentioned?: boolean;
38
+ }
39
+ /**
40
+ * Thin Host Resource for Plugin Runtime outbound template compilation.
41
+ * Evaluates `${expr}` template expressions in a sandboxed VM context
42
+ * (same security boundary as the legacy ComponentFeature pipeline).
43
+ *
44
+ * Implemented by the CLI Host; absent when not wired — consumers must
45
+ * pass text through unchanged.
46
+ */
47
+ export interface ComponentHost {
48
+ compileTemplate(text: string, context: TemplateContext): string;
49
+ }
50
+ export declare const componentHostToken: import("./token.js").Token<ComponentHost>;
@@ -0,0 +1,2 @@
1
+ import { createToken } from './token.js';
2
+ export const componentHostToken = createToken('zhin.component.host', 'Plugin Runtime template compilation host');
package/lib/identity.d.ts CHANGED
@@ -18,6 +18,15 @@ export declare function tokenId(value: string): TokenId;
18
18
  export declare function featureId(value: string): FeatureId;
19
19
  export declare function rootPluginId(): PluginId;
20
20
  export declare function childPluginId(parent: PluginId, instanceKey: string): PluginId;
21
+ /**
22
+ * Capability localName 的单段校验(`/` 分隔前的一段)。
23
+ *
24
+ * - `$name`:动态参数段(ASCII)
25
+ * - ASCII kebab / snake:`hello` / `lottery-today` / `voice_stt`
26
+ * - Unicode 名:须含至少一个非 ASCII 字符,且不得含 ASCII 大写(拉丁仍走 kebab/snake)
27
+ */
28
+ export declare function isCapabilityLocalSegment(segment: string): boolean;
29
+ export declare function isCapabilityLocalName(localName: string): boolean;
21
30
  /**
22
31
  * Database-safe owner encoding used by process-wide plugin resources.
23
32
  *
package/lib/identity.js CHANGED
@@ -1,7 +1,14 @@
1
1
  const namespacePattern = /^[a-z][a-z0-9.-]*$/;
2
+ /** Plugin instanceKey 仍限 ASCII kebab(配置键 / PluginId 路径段)。 */
2
3
  const localNamePattern = /^[a-z0-9][a-z0-9-]*$/;
3
- const capabilityLocalSegment = String.raw `(?:[a-z0-9][a-z0-9-]*|\$[a-z][a-zA-Z0-9]*)`;
4
- const capabilityLocalNamePattern = new RegExp(`^${capabilityLocalSegment}(?:/${capabilityLocalSegment})*$`);
4
+ /** 动态参数段(`[name].ts` → `$name`),标识符仍为 ASCII。 */
5
+ const dynamicCapabilitySegment = /^\$[a-z][a-zA-Z0-9]*$/u;
6
+ /**
7
+ * 静态 Capability / 命令路径段:ASCII kebab 或 snake(Host / agent tools 如 `voice_stt`),
8
+ * 或含非 ASCII 字母的 Unicode 名(如 `赞我`)。
9
+ */
10
+ const asciiNameSegment = /^[a-z0-9][a-z0-9_-]*$/u;
11
+ const unicodeNameSegment = /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u;
5
12
  function assertNamespace(value, label) {
6
13
  if (!namespacePattern.test(value)) {
7
14
  throw new TypeError(`Invalid ${label}: ${value}`);
@@ -24,6 +31,31 @@ export function childPluginId(parent, instanceKey) {
24
31
  }
25
32
  return `${parent}/${instanceKey}`;
26
33
  }
34
+ /**
35
+ * Capability localName 的单段校验(`/` 分隔前的一段)。
36
+ *
37
+ * - `$name`:动态参数段(ASCII)
38
+ * - ASCII kebab / snake:`hello` / `lottery-today` / `voice_stt`
39
+ * - Unicode 名:须含至少一个非 ASCII 字符,且不得含 ASCII 大写(拉丁仍走 kebab/snake)
40
+ */
41
+ export function isCapabilityLocalSegment(segment) {
42
+ if (!segment || segment.includes('/') || segment.includes('\0'))
43
+ return false;
44
+ if (dynamicCapabilitySegment.test(segment))
45
+ return true;
46
+ if (asciiNameSegment.test(segment))
47
+ return true;
48
+ if (/[A-Z]/.test(segment))
49
+ return false;
50
+ if (!/[^\x00-\x7F]/u.test(segment))
51
+ return false;
52
+ return unicodeNameSegment.test(segment);
53
+ }
54
+ export function isCapabilityLocalName(localName) {
55
+ if (!localName)
56
+ return false;
57
+ return localName.split('/').every(isCapabilityLocalSegment);
58
+ }
27
59
  /**
28
60
  * Database-safe owner encoding used by process-wide plugin resources.
29
61
  *
@@ -38,7 +70,7 @@ export function pluginOwnerResourceKey(owner) {
38
70
  .join('_')}_`;
39
71
  }
40
72
  export function capabilityId(owner, feature, localName) {
41
- if (!capabilityLocalNamePattern.test(localName)) {
73
+ if (!isCapabilityLocalName(localName)) {
42
74
  throw new TypeError(`Invalid capability local name: ${localName}`);
43
75
  }
44
76
  return `${owner}\0${feature}\0${localName}`;
package/lib/index.d.ts CHANGED
@@ -16,10 +16,12 @@ export * from './token.js';
16
16
  export * from './schedule-host.js';
17
17
  /** @public Host token:`databaseHostToken`(承诺 semver)。 */
18
18
  export * from './database-host.js';
19
- /** @public Host token:`agentToolsHostToken`(experimental)。 */
20
- export * from './agent-tools-host.js';
21
19
  export * from './html-renderer.js';
20
+ /** @public Host token:`componentHostToken`(承诺 semver)。 */
21
+ export * from './component-host.js';
22
22
  /** @public Host token:`outboundHostToken`(承诺 semver)。 */
23
23
  export * from './outbound-host.js';
24
+ /** @public 跨通道发送辅助:`createOutboundSender` / `sendTo`(承诺 semver)。 */
25
+ export * from './outbound-sender.js';
24
26
  export * from './inbox.js';
25
27
  export * from './system-log.js';
package/lib/index.js CHANGED
@@ -16,10 +16,12 @@ export * from './token.js';
16
16
  export * from './schedule-host.js';
17
17
  /** @public Host token:`databaseHostToken`(承诺 semver)。 */
18
18
  export * from './database-host.js';
19
- /** @public Host token:`agentToolsHostToken`(experimental)。 */
20
- export * from './agent-tools-host.js';
21
19
  export * from './html-renderer.js';
20
+ /** @public Host token:`componentHostToken`(承诺 semver)。 */
21
+ export * from './component-host.js';
22
22
  /** @public Host token:`outboundHostToken`(承诺 semver)。 */
23
23
  export * from './outbound-host.js';
24
+ /** @public 跨通道发送辅助:`createOutboundSender` / `sendTo`(承诺 semver)。 */
25
+ export * from './outbound-sender.js';
24
26
  export * from './inbox.js';
25
27
  export * from './system-log.js';
@@ -19,13 +19,13 @@ export interface OutboundConversation {
19
19
  */
20
20
  export interface OutboundSendInput {
21
21
  readonly adapter: string;
22
- readonly endpointId: string;
22
+ readonly endpointKey: string;
23
23
  readonly conversation: OutboundConversation;
24
24
  readonly content: string;
25
25
  }
26
26
  export interface OutboundReactionInput {
27
27
  readonly adapter: string;
28
- readonly endpointId: string;
28
+ readonly endpointKey: string;
29
29
  readonly messageId: string;
30
30
  readonly emoji: string;
31
31
  readonly sceneType?: string;
@@ -33,13 +33,13 @@ export interface OutboundReactionInput {
33
33
  }
34
34
  export interface OutboundRemoveReactionInput {
35
35
  readonly adapter: string;
36
- readonly endpointId: string;
36
+ readonly endpointKey: string;
37
37
  readonly messageId: string;
38
38
  readonly reactionId: string;
39
39
  }
40
40
  export interface OutboundRecallInput {
41
41
  readonly adapter: string;
42
- readonly endpointId: string;
42
+ readonly endpointKey: string;
43
43
  readonly messageId: string;
44
44
  }
45
45
  export interface OutboundHost {
@@ -0,0 +1,70 @@
1
+ import type { Token } from './token.js';
2
+ import { type OutboundConversation, type OutboundHost, type OutboundSendInput } from './outbound-host.js';
3
+ /**
4
+ * 构造群临时会话地址(如 QQ 群临时私聊)。
5
+ *
6
+ * ```ts
7
+ * await message.$sendTo(tempSession('12345', '67890'), '通过群临时会话私信');
8
+ * await sender.send(tempSession('12345', '67890'), '通知');
9
+ * ```
10
+ */
11
+ export declare function tempSession(userId: string, fromGroupId: string): OutboundConversation;
12
+ /**
13
+ * 构造频道子通道地址(如 QQ 频道内的子频道)。
14
+ *
15
+ * ```ts
16
+ * await message.$sendTo(guildChannel('channel-1', 'guild-1'), '频道通知');
17
+ * ```
18
+ */
19
+ export declare function guildChannel(channelId: string, guildId: string, threadId?: string): OutboundConversation;
20
+ /**
21
+ * 结构兼容 CapabilityContext.use()、Scope.use()、EndpointCommandUse 的最小访问器。
22
+ */
23
+ export type UseAccessor = <T>(token: Token<T>) => T;
24
+ /**
25
+ * 预绑定 adapter + endpoint 的发送器。
26
+ *
27
+ * ```ts
28
+ * const sender = createOutboundSender(context.use, 'root/icqq', 'main');
29
+ * await sender.send('group', '67890', 'hello');
30
+ * await sender.send({ kind: 'private', id: '12345' }, 'DM');
31
+ * ```
32
+ */
33
+ export interface OutboundSender {
34
+ send(kind: OutboundConversation['kind'], id: string, content: string): Promise<string | null>;
35
+ send(conversation: OutboundConversation, content: string): Promise<string | null>;
36
+ readonly host: OutboundHost;
37
+ readonly adapter: string;
38
+ readonly endpointKey: string;
39
+ }
40
+ /**
41
+ * 创建预绑定 adapter + endpoint 的发送器。
42
+ *
43
+ * 在 defineCommand 中:
44
+ * ```ts
45
+ * const sender = createOutboundSender(context.use, 'root/telegram', 'main');
46
+ * await sender.send('group', 'chat123', 'hello');
47
+ * ```
48
+ *
49
+ * 在 definePlugin setup 中:
50
+ * ```ts
51
+ * const sender = createOutboundSender(
52
+ * (token) => context.resources.use(token),
53
+ * config.adapter,
54
+ * config.endpointKey,
55
+ * );
56
+ * ```
57
+ */
58
+ export declare function createOutboundSender(use: UseAccessor, adapter: string, endpointKey: string): OutboundSender;
59
+ /**
60
+ * 一次性发送:解析 OutboundHost 并发送到指定目标。
61
+ *
62
+ * ```ts
63
+ * await sendTo(context.use, {
64
+ * adapter: 'root/icqq',
65
+ * endpointKey: 'main',
66
+ * conversation: { kind: 'group', id: '67890' },
67
+ * }, 'hello');
68
+ * ```
69
+ */
70
+ export declare function sendTo(use: UseAccessor, target: Omit<OutboundSendInput, 'content'>, content: string): Promise<string | null>;
@@ -0,0 +1,85 @@
1
+ import { outboundHostToken } from './outbound-host.js';
2
+ // ── 会话地址构造器 ──────────────────────────────────────────────────
3
+ /**
4
+ * 构造群临时会话地址(如 QQ 群临时私聊)。
5
+ *
6
+ * ```ts
7
+ * await message.$sendTo(tempSession('12345', '67890'), '通过群临时会话私信');
8
+ * await sender.send(tempSession('12345', '67890'), '通知');
9
+ * ```
10
+ */
11
+ export function tempSession(userId, fromGroupId) {
12
+ return { kind: 'private', id: userId, parent: { kind: 'group', id: fromGroupId } };
13
+ }
14
+ /**
15
+ * 构造频道子通道地址(如 QQ 频道内的子频道)。
16
+ *
17
+ * ```ts
18
+ * await message.$sendTo(guildChannel('channel-1', 'guild-1'), '频道通知');
19
+ * ```
20
+ */
21
+ export function guildChannel(channelId, guildId, threadId) {
22
+ return {
23
+ kind: 'channel',
24
+ id: channelId,
25
+ parent: { kind: 'channel', id: guildId },
26
+ ...(threadId ? { threadId } : {}),
27
+ };
28
+ }
29
+ /**
30
+ * 创建预绑定 adapter + endpoint 的发送器。
31
+ *
32
+ * 在 defineCommand 中:
33
+ * ```ts
34
+ * const sender = createOutboundSender(context.use, 'root/telegram', 'main');
35
+ * await sender.send('group', 'chat123', 'hello');
36
+ * ```
37
+ *
38
+ * 在 definePlugin setup 中:
39
+ * ```ts
40
+ * const sender = createOutboundSender(
41
+ * (token) => context.resources.use(token),
42
+ * config.adapter,
43
+ * config.endpointKey,
44
+ * );
45
+ * ```
46
+ */
47
+ export function createOutboundSender(use, adapter, endpointKey) {
48
+ const host = use(outboundHostToken);
49
+ return Object.freeze({
50
+ host,
51
+ adapter,
52
+ endpointKey,
53
+ send(kindOrConversation, idOrContent, maybeContent) {
54
+ if (typeof kindOrConversation === 'string' && maybeContent !== undefined) {
55
+ return host.send({
56
+ adapter,
57
+ endpointKey,
58
+ conversation: { kind: kindOrConversation, id: idOrContent },
59
+ content: maybeContent,
60
+ });
61
+ }
62
+ return host.send({
63
+ adapter,
64
+ endpointKey,
65
+ conversation: kindOrConversation,
66
+ content: idOrContent,
67
+ });
68
+ },
69
+ });
70
+ }
71
+ /**
72
+ * 一次性发送:解析 OutboundHost 并发送到指定目标。
73
+ *
74
+ * ```ts
75
+ * await sendTo(context.use, {
76
+ * adapter: 'root/icqq',
77
+ * endpointKey: 'main',
78
+ * conversation: { kind: 'group', id: '67890' },
79
+ * }, 'hello');
80
+ * ```
81
+ */
82
+ export async function sendTo(use, target, content) {
83
+ const host = use(outboundHostToken);
84
+ return host.send({ ...target, content });
85
+ }
package/lib/snapshot.d.ts CHANGED
@@ -32,11 +32,13 @@ export declare class GenerationConflictError extends Error {
32
32
  readonly actual: number;
33
33
  constructor(expected: number, actual: number);
34
34
  }
35
+ declare const snapshotLeaseAuthority: unique symbol;
35
36
  export declare class SnapshotLease {
36
37
  #private;
37
38
  readonly value: RuntimeSnapshot;
38
39
  private readonly releaseRecord;
39
- constructor(value: RuntimeSnapshot, releaseRecord: () => void);
40
+ constructor(value: RuntimeSnapshot, releaseRecord: () => void, authority: typeof snapshotLeaseAuthority);
41
+ get active(): boolean;
40
42
  release(): void;
41
43
  }
42
44
  export declare class SnapshotStore {
@@ -44,7 +46,9 @@ export declare class SnapshotStore {
44
46
  constructor(initial: SnapshotState);
45
47
  get current(): RuntimeSnapshot;
46
48
  acquire(): SnapshotLease;
49
+ owns(lease: SnapshotLease): boolean;
47
50
  commit(expectedGeneration: number, prepared: PreparedGeneration): RuntimeSnapshot;
48
51
  close(): Promise<void>;
49
52
  }
50
53
  export declare function createSnapshotView(generation: number, state: SnapshotState): RuntimeSnapshot;
54
+ export {};
package/lib/snapshot.js CHANGED
@@ -8,13 +8,19 @@ export class GenerationConflictError extends Error {
8
8
  this.name = 'GenerationConflictError';
9
9
  }
10
10
  }
11
+ const snapshotLeaseAuthority = Symbol('SnapshotLeaseAuthority');
11
12
  export class SnapshotLease {
12
13
  value;
13
14
  releaseRecord;
14
15
  #released = false;
15
- constructor(value, releaseRecord) {
16
+ constructor(value, releaseRecord, authority) {
16
17
  this.value = value;
17
18
  this.releaseRecord = releaseRecord;
19
+ if (authority !== snapshotLeaseAuthority)
20
+ throw new TypeError('SnapshotLease is owned by SnapshotStore');
21
+ }
22
+ get active() {
23
+ return !this.#released;
18
24
  }
19
25
  release() {
20
26
  if (this.#released)
@@ -26,6 +32,7 @@ export class SnapshotLease {
26
32
  export class SnapshotStore {
27
33
  #current;
28
34
  #retired = new Set();
35
+ #leases = new WeakSet();
29
36
  #closed = false;
30
37
  constructor(initial) {
31
38
  this.#current = {
@@ -43,10 +50,15 @@ export class SnapshotStore {
43
50
  throw new Error('SnapshotStore is closed');
44
51
  const record = this.#current;
45
52
  record.leases += 1;
46
- return new SnapshotLease(record.snapshot, () => {
53
+ const lease = new SnapshotLease(record.snapshot, () => {
47
54
  record.leases -= 1;
48
55
  this.#disposeIfReady(record);
49
- });
56
+ }, snapshotLeaseAuthority);
57
+ this.#leases.add(lease);
58
+ return lease;
59
+ }
60
+ owns(lease) {
61
+ return this.#leases.has(lease);
50
62
  }
51
63
  commit(expectedGeneration, prepared) {
52
64
  // The caller owns a candidate until this method succeeds. This lets the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-runtime",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Zero-dependency Plugin tree, Scope and atomic generation runtime for Zhin",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,38 +0,0 @@
1
- /**
2
- * Plugin-contributed Agent tool registered against the Agent Host.
3
- * Captured at setup() time — execute closures must never call plugin locators.
4
- *
5
- * Access vocabulary (platforms / scopes / permissions / hidden) mirrors the
6
- * `@zhin.js/tool` ToolIndex descriptor and is enforced per message by the
7
- * Agent runtime via Core `canAccessTool` — one predicate for both paths.
8
- */
9
- export interface AgentToolRegistration {
10
- /** Runtime tool name exposed to the model (e.g. `lottery_sync`). */
11
- readonly name: string;
12
- readonly description: string;
13
- /** zod object schema or JSON Schema; drives the deferred catalog + arg validation. */
14
- readonly inputSchema?: unknown;
15
- readonly keywords?: readonly string[];
16
- readonly tags?: readonly string[];
17
- /** Limit to these adapter platforms (e.g. `['icqq']`); empty = all. */
18
- readonly platforms?: readonly string[];
19
- /** Limit to these channel scopes; empty = all. */
20
- readonly scopes?: readonly ('private' | 'group' | 'channel')[];
21
- readonly permissions?: readonly string[];
22
- /** Registered but never offered to the model (callable by name only). */
23
- readonly hidden?: boolean;
24
- /** Per-tool approval policy; stacks with ExecPolicy. */
25
- readonly approval?: 'never' | 'always';
26
- /** Plugin/tool origin label (e.g. plugin name) for diagnostics. */
27
- readonly source?: string;
28
- execute(input: Record<string, unknown>): unknown | Promise<unknown>;
29
- }
30
- /**
31
- * Generation-owned Agent tool registry provided by the Agent Host.
32
- * Absent when AI is not installed/enabled — plugins must guard with `has()`.
33
- */
34
- export interface AgentToolsHost {
35
- /** Register a tool for every Agent turn of this generation; returns disposal. */
36
- register(tool: AgentToolRegistration): () => void;
37
- }
38
- export declare const agentToolsHostToken: import("./token.js").Token<AgentToolsHost>;
@@ -1,2 +0,0 @@
1
- import { createToken } from './token.js';
2
- export const agentToolsHostToken = createToken('zhin.agent-tools.host', 'Plugin Runtime agent tools host');