feat(mcp): complete action results and invocation examples
Document result shapes and practical calls for all built-in actions, including prerequisites, streaming workflows and compatibility limits. Preserve existing interfaces and execution behavior, and verify the catalog against action validation and representative responses.
This commit is contained in:
@@ -101,7 +101,9 @@ WebUI deployment targets use a reversible presentation overlay. Read [the host a
|
||||
|
||||
## Generated files and related changes
|
||||
|
||||
OneBot action definitions own validation and catalog metadata. After changing an action name, parameter, result description or execution mode:
|
||||
OneBot action definitions own validation and catalog metadata. Each bundled action supplies a `returnsSchema` for successful OneBot `data` and at least one `examples` entry with valid parameters and any required identifier sources. Stream schemas describe individual successful frames, not MCP aggregated results. Keep undocumented upstream fields open rather than inventing a structure, and label unsupported or placeholder behavior explicitly.
|
||||
|
||||
After changing an action name, parameter, result description/schema, example or execution mode:
|
||||
|
||||
1. Update the owning definition in `packages/onebot/src/actions` and its tests.
|
||||
2. Run `pnpm --filter @snowluma/mcp gen`.
|
||||
|
||||
@@ -5,6 +5,48 @@ and read/write gates. No endpoint is contacted by catalog tools or by a local
|
||||
runtime overview. The action catalog remains a bundled snapshot, not a claim
|
||||
about the connected server's capabilities.
|
||||
|
||||
## Action result schemas and examples
|
||||
|
||||
`get_action` and `snowluma://onebot/actions` include `returnsSchema` and
|
||||
`examples` for every bundled action, including aliases. Both are authored beside
|
||||
the OneBot action definition and copied into the generated catalog. List/search
|
||||
responses remain compact; use `get_action` to retrieve the complete contract.
|
||||
|
||||
`returnsSchema` describes successful OneBot **`data`**, not the full OneBot
|
||||
envelope or the MCP `structuredContent` wrapper. Check `status` and `retcode`
|
||||
before reading it. For stream actions, the schema describes each successful
|
||||
frame's `data`; MCP downloads instead return saved-file metadata and generic
|
||||
streams return a frame collection. Error frames and failed envelopes are not
|
||||
successful results. Unsupported actions use `{ "not": {} }` because they have no
|
||||
successful result. Compatibility placeholders explicitly document their limits.
|
||||
|
||||
Each example has `summary`, `params`, and optional `notes` explaining prerequisites
|
||||
and identifier sources. These are illustrative calls, never run by catalog tools.
|
||||
Replace example account/group IDs, opaque handles and paths with values from the
|
||||
current account before execution. Notes identify operations with multiple steps,
|
||||
such as completing an upload after all chunks have been acknowledged.
|
||||
|
||||
For example, `get_action({ "name": "get_msg" })` documents signed message IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get_msg",
|
||||
"params": { "message_id": -123456 }
|
||||
}
|
||||
```
|
||||
|
||||
Pass this object to `query_action` with a real message ID. Writes and streams use
|
||||
`invoke_action` in write mode. For automatic upload, pass
|
||||
`{ "action": "upload_file_stream", "execution": { "input_file": "/allowed/hello.txt" } }`
|
||||
to `invoke_action`, with a real file inside the configured upload root. Ordinary
|
||||
action paths refer to the SnowLuma server, whereas `execution.input_file` and MCP
|
||||
download paths refer to the MCP host.
|
||||
|
||||
Do not substitute zero for a signed message ID. Resource-only forward uploads
|
||||
and flash shares have a documented `message_id: 0` sentinel; it cannot be used
|
||||
for message lookup or recall. Also preserve `null` versus `{}`, optional fields,
|
||||
and decimal strings such as media `file_size` exactly as documented.
|
||||
|
||||
## Structured results
|
||||
|
||||
Every tool advertises an `outputSchema` and returns `structuredContent` alongside
|
||||
|
||||
@@ -18,6 +18,8 @@ it('generates src/generated/catalog.ts from the live action specs', () => {
|
||||
for (const a of actions) {
|
||||
expect(a.inputSchema).toBeTypeOf('object');
|
||||
expect(a.name).toBeTruthy();
|
||||
expect(a.returnsSchema, `${a.name} result schema`).toBeTypeOf('object');
|
||||
expect(a.examples?.length, `${a.name} invocation examples`).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
const body =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -66,7 +66,8 @@ const DOCS_TOOLS: Tool[] = [
|
||||
},
|
||||
{
|
||||
name: 'get_action',
|
||||
description: '获取 action 文档、参数 JSON Schema、约束和返回类型。',
|
||||
description:
|
||||
'获取 action 文档、参数与返回数据 JSON Schema、约束和调用示例。示例中的标识和路径需替换;流式返回结构描述单帧数据。',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string', description: 'action 名(接受别名)' } },
|
||||
|
||||
@@ -26,8 +26,15 @@ export interface CatalogAction {
|
||||
category?: string;
|
||||
summary?: string;
|
||||
returns?: string;
|
||||
/** JSON Schema for the action's `data` payload (absent when undocumented). */
|
||||
/** Successful OneBot `data`, or one successful stream frame's `data`.
|
||||
* Does not describe the envelope or MCP's aggregated stream result. */
|
||||
returnsSchema?: Record<string, unknown>;
|
||||
/** Illustrative OneBot params; identifiers/paths must be replaced before use. */
|
||||
examples?: ReadonlyArray<{
|
||||
summary: string;
|
||||
params: Record<string, unknown>;
|
||||
notes?: string;
|
||||
}>;
|
||||
/** True only for pure data-fetch actions (no side effects). Drives the
|
||||
* read/write tool routing: read-only → query_action, else → invoke_action.
|
||||
* Classified at the source spec by what the action's `run` actually does. */
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';
|
||||
import { collectActionDocs } from '@snowluma/onebot/action-docs';
|
||||
import { ApiHandler, type ApiActionContext } from '@snowluma/onebot/api-handler';
|
||||
import type { JsonObject, JsonValue } from '@snowluma/onebot/types';
|
||||
import { ACTIONS } from '../src/generated/catalog';
|
||||
import { callTool } from '../src/tools';
|
||||
|
||||
const validator = new AjvJsonSchemaValidator();
|
||||
|
||||
function resultValidator(name: string) {
|
||||
const action = ACTIONS.find((entry) => entry.name === name);
|
||||
|
||||
if (!action?.returnsSchema) {
|
||||
throw new Error(`missing result schema: ${name}`);
|
||||
}
|
||||
|
||||
return validator.getValidator(action.returnsSchema);
|
||||
}
|
||||
|
||||
function matchesResult(name: string, data: unknown): void {
|
||||
const result = resultValidator(name)(data);
|
||||
expect(result.valid, `${name}: ${result.errorMessage}`).toBe(true);
|
||||
}
|
||||
|
||||
/** Unexpected dependencies fail rather than contacting an account or a network. */
|
||||
function context(values: Record<string, unknown> = {}): ApiActionContext {
|
||||
return new Proxy(values, {
|
||||
get(target, key) {
|
||||
if (typeof key === 'string' && Object.hasOwn(target, key)) {
|
||||
return target[key];
|
||||
}
|
||||
|
||||
throw new Error(`unstubbed action context: ${String(key)}`);
|
||||
},
|
||||
}) as unknown as ApiActionContext;
|
||||
}
|
||||
|
||||
describe('bundled action contracts', () => {
|
||||
it('matches the owning specs, without relying on a second hand-authored registry', () => {
|
||||
expect(ACTIONS).toEqual(JSON.parse(JSON.stringify(collectActionDocs())));
|
||||
});
|
||||
|
||||
it.each(ACTIONS)('$name publishes compilable schemas and schema-valid examples', (action) => {
|
||||
expect(() => resultValidator(action.name)).not.toThrow();
|
||||
const validateInput = validator.getValidator(action.inputSchema);
|
||||
expect(action.examples?.length).toBeGreaterThan(0);
|
||||
|
||||
for (const example of action.examples ?? []) {
|
||||
const result = validateInput(example.params);
|
||||
expect(result.valid, `${action.name}: ${result.errorMessage}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('exposes full contracts through get_action and aliases without executing anything', async () => {
|
||||
const client = { call: vi.fn(), callStream: vi.fn() };
|
||||
|
||||
for (const action of ACTIONS) {
|
||||
for (const name of [action.name, ...action.aliases]) {
|
||||
const result = await callTool('get_action', { name }, { mode: 'docs', client });
|
||||
expect(result.structuredContent.result).toEqual(action);
|
||||
expect(result.content[0]).toEqual({
|
||||
type: 'text',
|
||||
text: JSON.stringify(action, null, 2),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
expect(client.call).not.toHaveBeenCalled();
|
||||
expect(client.callStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['list_actions', 'search_actions'])('keeps %s compact', async (name) => {
|
||||
const result = await callTool(name, { query: 'send_group_msg' }, { mode: 'docs' });
|
||||
const block = result.content[0];
|
||||
expect(block.type).toBe('text');
|
||||
|
||||
if (block.type !== 'text') {
|
||||
throw new Error('expected text result');
|
||||
}
|
||||
|
||||
const payload = JSON.parse(block.text);
|
||||
expect(payload.actions.length).toBeGreaterThan(0);
|
||||
|
||||
for (const action of payload.actions) {
|
||||
expect(action).not.toHaveProperty('examples');
|
||||
expect(action).not.toHaveProperty('returnsSchema');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('result schemas against actual OneBot handlers', () => {
|
||||
it('preserves signed message IDs and documents the upload-only sentinel separately', async () => {
|
||||
const api = new ApiHandler(
|
||||
context({
|
||||
sendGroupMessage: async () => ({ messageId: -123 }),
|
||||
sendForwardMsg: async () => ({ forwardId: 'example-forward' }),
|
||||
}),
|
||||
);
|
||||
const send = await api.handle('send_group_msg', { group_id: 20001, message: 'hello' });
|
||||
expect(send).toEqual({ status: 'ok', retcode: 0, data: { message_id: -123 } });
|
||||
matchesResult('send_group_msg', send.data);
|
||||
expect(resultValidator('send_group_msg')({ message_id: 0 }).valid).toBe(false);
|
||||
|
||||
for (const name of ['upload_forward_msg', 'upload_foward_msg']) {
|
||||
const uploaded = await api.handle(name, {
|
||||
group_id: 20001,
|
||||
messages: [
|
||||
{
|
||||
type: 'node',
|
||||
data: { user_id: 10001, nickname: 'example', content: 'hello' },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(uploaded.status).toBe('ok');
|
||||
expect(uploaded.data).toMatchObject({ message_id: 0, res_id: 'example-forward' });
|
||||
matchesResult(name, uploaded.data);
|
||||
|
||||
if (name === 'upload_forward_msg') {
|
||||
expect(uploaded.data).toHaveProperty('group_id', 20001);
|
||||
} else {
|
||||
expect(uploaded.data).not.toHaveProperty('group_id');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('distinguishes null, empty objects, and failed unsupported calls', async () => {
|
||||
const api = new ApiHandler(
|
||||
context({
|
||||
bridge: {
|
||||
apis: {
|
||||
friend: { setRemark: async () => {} },
|
||||
profile: { setInputStatus: async () => {} },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const empty = await api.handle('set_friend_remark', { user_id: 10001, remark: 'example' });
|
||||
expect(empty.data).toBeNull();
|
||||
matchesResult('set_friend_remark', empty.data);
|
||||
expect(resultValidator('set_friend_remark')({}).valid).toBe(false);
|
||||
const object = await api.handle('set_input_status', { user_id: 10001 });
|
||||
expect(object.data).toEqual({});
|
||||
matchesResult('set_input_status', object.data);
|
||||
expect(resultValidator('set_input_status')(null).valid).toBe(false);
|
||||
|
||||
for (const name of ['set_restart', 'create_collection', '.get_word_slices']) {
|
||||
const failure = await api.handle(name, {});
|
||||
expect(failure.status).toBe('failed');
|
||||
expect(resultValidator(name)(failure.data).valid).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('documents actual media string sizes and optional converted record fields', async () => {
|
||||
const media = {
|
||||
file: 'https://example.com/media',
|
||||
url: 'https://example.com/media',
|
||||
file_size: '42',
|
||||
file_name: 'voice.amr',
|
||||
};
|
||||
const api = new ApiHandler(
|
||||
context({
|
||||
getImageInfo: async () => media,
|
||||
getRecordInfo: async () => media,
|
||||
bridge: {
|
||||
apis: {
|
||||
extras: { convertRecord: async () => ({ base64: 'aGVsbG8=', size: 5 }) },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
for (const name of ['get_image', 'get_file', 'get_record']) {
|
||||
const result = await api.handle(name, { file: 'cached-media' });
|
||||
expect(result.status).toBe('ok');
|
||||
matchesResult(name, result.data);
|
||||
expect(resultValidator(name)({ ...media, file_size: 42 }).valid).toBe(false);
|
||||
}
|
||||
|
||||
const converted = await api.handle('get_record', {
|
||||
file: 'cached-media',
|
||||
out_format: 'mp3',
|
||||
});
|
||||
expect(converted.data).toEqual({ ...media, out_format: 'mp3', base64: 'aGVsbG8=' });
|
||||
matchesResult('get_record', converted.data);
|
||||
});
|
||||
|
||||
it('describes get_msg data without the event envelope for both conversation types', async () => {
|
||||
for (const messageType of ['group', 'private']) {
|
||||
const event: JsonObject = {
|
||||
post_type: 'message',
|
||||
self_id: 10002,
|
||||
message_type: messageType,
|
||||
message_id: -123,
|
||||
user_id: 10001,
|
||||
time: 1700000000,
|
||||
message: [{ type: 'text', data: { text: 'hello' } }],
|
||||
raw_message: 'hello',
|
||||
...(messageType === 'group' ? { group_id: 20001 } : { target_id: 10002 }),
|
||||
};
|
||||
const api = new ApiHandler(
|
||||
context({
|
||||
getMessage: async () => event,
|
||||
refreshMessageMedia: async () => {},
|
||||
}),
|
||||
);
|
||||
const result = await api.handle('get_msg', { message_id: -123 });
|
||||
expect(result.status).toBe('ok');
|
||||
expect(result.data).toMatchObject({ real_id: -123, message_id: -123 });
|
||||
expect(result.data).not.toHaveProperty('post_type');
|
||||
expect(result.data).not.toHaveProperty('self_id');
|
||||
matchesResult('get_msg', result.data);
|
||||
}
|
||||
});
|
||||
|
||||
it('describes successful stream frame data, not MCP aggregate results or failed frames', async () => {
|
||||
const api = new ApiHandler(context());
|
||||
const frames: JsonValue[] = [];
|
||||
const response = await api.handle(
|
||||
'test_download_stream',
|
||||
{ error: false },
|
||||
{
|
||||
send: async (frame) => {
|
||||
frames.push(frame);
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(frames).toHaveLength(10);
|
||||
expect(response.status).toBe('ok');
|
||||
|
||||
for (const frame of [...frames, response.data]) {
|
||||
matchesResult('test_download_stream', frame);
|
||||
}
|
||||
|
||||
expect(
|
||||
resultValidator('test_download_stream')({ frame_count: 11, frames, terminal: response })
|
||||
.valid,
|
||||
).toBe(false);
|
||||
expect(
|
||||
resultValidator('test_download_stream')({ type: 'error', data_type: 'error' }).valid,
|
||||
).toBe(false);
|
||||
const failure = await api.handle('test_download_stream', { error: true });
|
||||
expect(failure.status).toBe('failed');
|
||||
});
|
||||
});
|
||||
@@ -68,6 +68,21 @@ function renderAction(doc: ActionDoc): string {
|
||||
lines.push('', `**返回:** \`${doc.returns}\``);
|
||||
}
|
||||
|
||||
for (const example of doc.examples ?? []) {
|
||||
lines.push('', `**示例:${example.summary}**`);
|
||||
|
||||
if (example.notes) {
|
||||
lines.push('', example.notes);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'```json',
|
||||
JSON.stringify({ action: doc.name, params: example.params }, null, 2),
|
||||
'```',
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
||||
@@ -629,6 +629,16 @@ const RULES: RuleBuilders<Record<string, unknown>> = {
|
||||
export interface ParamDoc extends FieldDoc {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Documentation only: never execute examples while collecting the catalog. */
|
||||
export interface ActionExample {
|
||||
summary: string;
|
||||
/** OneBot params, also passed as the MCP action tool's `params`. */
|
||||
params: JsonObject;
|
||||
/** Prerequisites, placeholder sources, or ordering within a workflow. */
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ActionDoc {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
@@ -636,10 +646,11 @@ export interface ActionDoc {
|
||||
category?: string;
|
||||
summary?: string;
|
||||
returns?: string;
|
||||
/** JSON Schema for the `data` payload inside the OneBot envelope (NOT the
|
||||
* whole envelope). Optional + authored incrementally; absent ⇒ data shape
|
||||
* is documented only by the prose `returns`. */
|
||||
/** JSON Schema for successful OneBot `data`, not the envelope. For streams,
|
||||
* describes each successful frame, not an MCP aggregated tool result.
|
||||
* Optional for third-party specs; the bundled catalog requires coverage. */
|
||||
returnsSchema?: JsonSchema;
|
||||
examples?: readonly ActionExample[];
|
||||
/** True only for pure data-fetch actions (no side effects). Drives the MCP's
|
||||
* read/write tool routing; defaults to false (treated as a write). */
|
||||
readOnly: boolean;
|
||||
@@ -687,6 +698,7 @@ interface ActionDef<S extends Spec> {
|
||||
returns?: string;
|
||||
/** JSON Schema for the `data` payload (NOT the envelope). See ActionDoc. */
|
||||
returnsSchema?: JsonSchema;
|
||||
examples?: readonly ActionExample[];
|
||||
/** Mark true ONLY for pure data-fetch actions with no side effects. Default
|
||||
* false = write. Surfaced via describe() into the catalog for the MCP's
|
||||
* read/write routing. Classify by what `run` actually does, not the name. */
|
||||
@@ -775,6 +787,7 @@ export function defineAction<S extends Spec>(def: ActionDef<S>): ActionSpec<S> {
|
||||
summary: def.summary,
|
||||
returns: def.returns,
|
||||
returnsSchema: def.returnsSchema,
|
||||
...(def.examples ? { examples: def.examples } : {}),
|
||||
readOnly: def.readOnly ?? false,
|
||||
params: entries.map(([name, field]) => ({ name, ...field.doc })),
|
||||
invariants: rules.map((rule) => rule.doc),
|
||||
@@ -797,6 +810,7 @@ interface StreamActionDef<S extends Spec> {
|
||||
summary?: string;
|
||||
returns?: string;
|
||||
returnsSchema?: JsonSchema;
|
||||
examples?: readonly ActionExample[];
|
||||
readOnly?: boolean;
|
||||
params: S;
|
||||
rules?: (r: RuleBuilders<InferParams<S>>) => readonly CrossFieldRule[];
|
||||
@@ -823,6 +837,7 @@ export function defineStreamAction<S extends Spec>(def: StreamActionDef<S>): Str
|
||||
summary: def.summary,
|
||||
returns: def.returns,
|
||||
returnsSchema: def.returnsSchema,
|
||||
examples: def.examples,
|
||||
readOnly: def.readOnly,
|
||||
params: def.params,
|
||||
rules: def.rules,
|
||||
@@ -865,6 +880,7 @@ interface PresetDef<S extends Spec, Extra> {
|
||||
summary?: string;
|
||||
returns?: string;
|
||||
returnsSchema?: JsonSchema;
|
||||
examples?: readonly ActionExample[];
|
||||
readOnly?: boolean;
|
||||
params?: S;
|
||||
rules?: (r: RuleBuilders<InferParams<S> & Extra>) => readonly CrossFieldRule[];
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { JsonSchema } from './action-kit';
|
||||
|
||||
// Documentation fragments only. They never validate or transform runtime data.
|
||||
export const messageIdSchema: JsonSchema = {
|
||||
type: 'integer',
|
||||
minimum: -2147483648,
|
||||
maximum: 2147483647,
|
||||
not: { const: 0 },
|
||||
description: '非零有符号 32 位消息 ID,负值合法;必须原样保留',
|
||||
};
|
||||
|
||||
export const messageResultSchema: JsonSchema = {
|
||||
type: 'object',
|
||||
properties: { message_id: messageIdSchema },
|
||||
required: ['message_id'],
|
||||
};
|
||||
|
||||
export const messageSegmentSchema: JsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', description: '消息段类型,例如 text、image、reply' },
|
||||
data: {
|
||||
type: 'object',
|
||||
description: '字段随消息段类型变化,例如 text 段包含 text,reply 段包含 id',
|
||||
additionalProperties: true,
|
||||
},
|
||||
},
|
||||
required: ['type', 'data'],
|
||||
};
|
||||
|
||||
/** Stored events retain subtype-specific fields; legacy records can omit fields. */
|
||||
export const messageDataSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
time: { type: 'integer', description: '消息时间,Unix 秒' },
|
||||
message_type: { type: 'string', enum: ['private', 'group'] },
|
||||
sub_type: { type: 'string', description: '消息子类型' },
|
||||
message_id: messageIdSchema,
|
||||
real_id: { type: 'integer', description: '原始 message_id;旧历史记录缺失时可为 0' },
|
||||
user_id: { type: 'integer', description: '发送者 QQ 号' },
|
||||
group_id: { type: 'integer', description: '群聊消息的群号,私聊可省略' },
|
||||
target_id: { type: 'integer', description: '私聊目标 QQ 号(若有)' },
|
||||
message: { type: 'array', items: messageSegmentSchema },
|
||||
raw_message: { type: 'string', description: '消息的 CQ 码字符串形式' },
|
||||
font: { type: 'integer' },
|
||||
sender: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: { type: 'integer' },
|
||||
nickname: { type: 'string' },
|
||||
card: { type: 'string' },
|
||||
sex: { type: 'string', enum: ['male', 'female', 'unknown'] },
|
||||
age: { type: 'integer' },
|
||||
area: { type: 'string' },
|
||||
level: { type: 'string' },
|
||||
role: { type: 'string', enum: ['owner', 'admin', 'member'] },
|
||||
title: { type: 'string' },
|
||||
},
|
||||
additionalProperties: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: true,
|
||||
} satisfies JsonSchema;
|
||||
|
||||
// Forward nodes carry their original identifiers, not locally hashed message IDs.
|
||||
export const forwardNodeSchema: JsonSchema = {
|
||||
...messageDataSchema,
|
||||
properties: {
|
||||
...messageDataSchema.properties,
|
||||
self_id: { type: 'integer' },
|
||||
post_type: { const: 'message' },
|
||||
message_format: { const: 'array' },
|
||||
message_id: { type: 'integer', description: '转发节点原始标识,缺失时为 0' },
|
||||
message_seq: { type: 'integer', description: '节点消息序号,缺失时使用节点标识或 0' },
|
||||
real_id: { type: 'integer', description: '与节点 message_id 相同' },
|
||||
},
|
||||
required: [
|
||||
'self_id',
|
||||
'user_id',
|
||||
'time',
|
||||
'message_id',
|
||||
'message_seq',
|
||||
'real_id',
|
||||
'message_type',
|
||||
'sender',
|
||||
'raw_message',
|
||||
'font',
|
||||
'sub_type',
|
||||
'message',
|
||||
'message_format',
|
||||
'post_type',
|
||||
],
|
||||
};
|
||||
|
||||
export const mediaInfoSchema: JsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: { type: 'string', description: '媒体地址;图片无地址时可为原文件标识' },
|
||||
url: { type: 'string', description: '媒体下载地址,图片地址可能为空' },
|
||||
file_size: { type: 'string', description: '原媒体字节数的十进制字符串' },
|
||||
file_name: { type: 'string', description: '文件名或缓存标识' },
|
||||
},
|
||||
required: ['file', 'url', 'file_size', 'file_name'],
|
||||
};
|
||||
|
||||
export const flashFileSchema: JsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileset_id: { type: 'string' },
|
||||
file_name: { type: 'string' },
|
||||
orig_name: { type: 'string' },
|
||||
size: { type: 'number', description: '文件字节数;此接口字段名为 size' },
|
||||
share_url: { type: 'string' },
|
||||
file_id: { type: 'string' },
|
||||
download_url: { type: 'string' },
|
||||
},
|
||||
required: [
|
||||
'fileset_id',
|
||||
'file_name',
|
||||
'orig_name',
|
||||
'size',
|
||||
'share_url',
|
||||
'file_id',
|
||||
'download_url',
|
||||
],
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,12 @@ import { okResponse } from '../types';
|
||||
export const actions = [
|
||||
defineAction({
|
||||
name: 'get_friend_list',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取好友列表',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
summary: '获取好友列表',
|
||||
readOnly: true,
|
||||
returns: '好友列表数组,每项含 QQ 号、昵称与备注。',
|
||||
@@ -31,6 +37,15 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_stranger_info',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取陌生人信息',
|
||||
params: {
|
||||
user_id: 10001,
|
||||
},
|
||||
notes: '替换为需要查询的 QQ 号。',
|
||||
},
|
||||
],
|
||||
summary: '获取陌生人信息',
|
||||
readOnly: true,
|
||||
returns:
|
||||
@@ -109,6 +124,20 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'delete_friend',
|
||||
examples: [
|
||||
{
|
||||
summary: '删除好友',
|
||||
params: {
|
||||
user_id: 10001,
|
||||
block: false,
|
||||
},
|
||||
notes: '会删除指定好友;仅在明确需要删除时调用。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '删除好友',
|
||||
params: { user_id: f.userId().describe('QQ 号'), block: f.bool().default(false) },
|
||||
run: async (p, ctx) => {
|
||||
|
||||
@@ -4,6 +4,21 @@ import { okResponse } from '../types';
|
||||
export const actions = [
|
||||
groupUserAction({
|
||||
name: 'set_group_kick',
|
||||
examples: [
|
||||
{
|
||||
summary: '踢出群成员',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
user_id: 10001,
|
||||
reject_add_request: false,
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '踢出群成员',
|
||||
params: { reject_add_request: f.bool().default(false) },
|
||||
run: async (p, ctx) => {
|
||||
@@ -18,6 +33,21 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_kick_members',
|
||||
examples: [
|
||||
{
|
||||
summary: '批量踢出群成员',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
user_id: [10001, 10002],
|
||||
reject_add_request: false,
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '批量踢出群成员',
|
||||
params: {
|
||||
user_id: f.array(f.memberId()).nonEmpty(),
|
||||
@@ -35,6 +65,21 @@ export const actions = [
|
||||
|
||||
groupUserAction({
|
||||
name: 'set_group_ban',
|
||||
examples: [
|
||||
{
|
||||
summary: '禁言群成员(duration=0 解除)',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
user_id: 10001,
|
||||
duration: 600,
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。 duration 为秒,0 表示解除禁言。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '禁言群成员(duration=0 解除)',
|
||||
params: { duration: f.duration().default(1800) },
|
||||
run: async (p, ctx) => {
|
||||
@@ -45,6 +90,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_whole_ban',
|
||||
examples: [
|
||||
{
|
||||
summary: '全员禁言开关',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
enable: true,
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '全员禁言开关',
|
||||
params: { enable: f.bool().default(true) },
|
||||
run: async (p, ctx) => {
|
||||
@@ -55,6 +114,22 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_add_option',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置加群选项',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
add_type: 2,
|
||||
group_question: '入群用途?',
|
||||
group_answer: '学习交流',
|
||||
},
|
||||
notes: '修改加群设置前先用 get_group_admin_settings 查看当前状态,并确认目标设置值。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置加群选项',
|
||||
params: {
|
||||
add_type: f.int({ min: 0 }).default(0),
|
||||
@@ -74,6 +149,21 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_search',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置群被搜索方式(群指纹 / 群号搜索开关)',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
no_finger_open: 0,
|
||||
no_code_finger_open: 0,
|
||||
},
|
||||
notes: '替换群号;数值含义以当前群设置为准。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置群被搜索方式(群指纹 / 群号搜索开关)',
|
||||
params: {
|
||||
no_finger_open: f.int({ min: 0 }).optional(),
|
||||
@@ -91,6 +181,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_member_invite_policy',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置群成员邀请策略',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
policy: 'require_approval',
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置群成员邀请策略',
|
||||
params: {
|
||||
policy: f.enum('disabled', 'require_approval', 'no_approval', 'no_approval_under_100'),
|
||||
@@ -103,6 +207,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_new_member_history_visibility',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置新成员是否可查看历史消息',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
visible: true,
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置新成员是否可查看历史消息',
|
||||
params: { visible: f.bool() },
|
||||
run: async (p, ctx) => {
|
||||
@@ -113,6 +231,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_member_permissions',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置群成员权限(仅群主可改)',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
allow_member_upload_album: false,
|
||||
},
|
||||
notes: '至少传一个权限字段;仅修改传入的权限,其余保持原样。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置群成员权限(仅群主可改)',
|
||||
params: {
|
||||
allow_member_upload_album: f.bool().optional(),
|
||||
@@ -138,6 +270,15 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_admin_settings',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群管理设置的当前值',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群管理设置的当前值',
|
||||
readOnly: true,
|
||||
returns: '与对应设置接口字段对齐的当前群管理设置。',
|
||||
@@ -210,6 +351,15 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_group_tags',
|
||||
examples: [
|
||||
{
|
||||
summary: '读取某个群分类下的资料标签',
|
||||
params: {
|
||||
group_class: 1,
|
||||
},
|
||||
notes: 'group_class 使用实际群分类编号。',
|
||||
},
|
||||
],
|
||||
summary: '读取某个群分类下的资料标签',
|
||||
readOnly: true,
|
||||
returns: '该分类下的资料标签列表。',
|
||||
@@ -239,6 +389,20 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'add_group_tag',
|
||||
examples: [
|
||||
{
|
||||
summary: '向某个群分类添加资料标签',
|
||||
params: {
|
||||
group_class: 1,
|
||||
name: '学习',
|
||||
},
|
||||
notes: '替换实际群分类和标签;需要当前账号有管理权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '向某个群分类添加资料标签',
|
||||
params: {
|
||||
group_class: f.int({ min: 0, max: 0xffffffff }),
|
||||
@@ -252,6 +416,20 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'delete_group_tag',
|
||||
examples: [
|
||||
{
|
||||
summary: '从某个群分类移除资料标签',
|
||||
params: {
|
||||
group_class: 1,
|
||||
name: '学习',
|
||||
},
|
||||
notes: '替换实际群分类和标签;需要当前账号有管理权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '从某个群分类移除资料标签',
|
||||
params: {
|
||||
group_class: f.int({ min: 0, max: 0xffffffff }),
|
||||
@@ -265,6 +443,21 @@ export const actions = [
|
||||
|
||||
groupUserAction({
|
||||
name: 'set_group_admin',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置/取消管理员',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
user_id: 10001,
|
||||
enable: true,
|
||||
},
|
||||
notes: '需要群主权限;会授予该成员管理员身份。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置/取消管理员',
|
||||
params: { enable: f.bool().default(true) },
|
||||
run: async (p, ctx) => {
|
||||
@@ -275,6 +468,21 @@ export const actions = [
|
||||
|
||||
groupUserAction({
|
||||
name: 'set_group_card',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置群名片(空字符串清除)',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
user_id: 10001,
|
||||
card: '示例群名片',
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置群名片(空字符串清除)',
|
||||
params: { card: f.string().default('') },
|
||||
run: async (p, ctx) => {
|
||||
@@ -285,6 +493,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_name',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置群名',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
group_name: '学习交流群',
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置群名',
|
||||
params: { group_name: f.string().default('') },
|
||||
run: async (p, ctx) => {
|
||||
@@ -295,6 +517,19 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_leave',
|
||||
examples: [
|
||||
{
|
||||
summary: '退群',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
},
|
||||
notes: '会退出该群,请确认目标群号后调用。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '退群',
|
||||
run: async (p, ctx) => {
|
||||
await ctx.bridge.apis.groupAdmin.leave(p.group_id);
|
||||
@@ -304,6 +539,21 @@ export const actions = [
|
||||
|
||||
groupUserAction({
|
||||
name: 'set_group_special_title',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置群头衔',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
user_id: 10001,
|
||||
special_title: '学习委员',
|
||||
},
|
||||
notes: '替换群号和成员 QQ 号;操作是否允许取决于当前账号在该群的权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置群头衔',
|
||||
params: { special_title: f.string().default('') },
|
||||
run: async (p, ctx) => {
|
||||
@@ -319,12 +569,34 @@ export const actions = [
|
||||
// No-op stubs (accept and ignore any params, as before).
|
||||
defineAction({
|
||||
name: 'set_group_anonymous',
|
||||
examples: [
|
||||
{
|
||||
summary: '匿名开关(未实现,返回 ok)',
|
||||
params: {},
|
||||
notes: '兼容占位接口:当前返回成功但不会修改匿名设置。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '匿名开关(未实现,返回 ok)',
|
||||
params: {},
|
||||
run: () => okResponse(),
|
||||
}),
|
||||
defineAction({
|
||||
name: 'set_group_anonymous_ban',
|
||||
examples: [
|
||||
{
|
||||
summary: '匿名禁言(未实现,返回 ok)',
|
||||
params: {},
|
||||
notes: '兼容占位接口:当前返回成功但不会修改匿名设置。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '匿名禁言(未实现,返回 ok)',
|
||||
params: {},
|
||||
run: () => okResponse(),
|
||||
@@ -332,6 +604,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_portrait',
|
||||
examples: [
|
||||
{
|
||||
summary: '设置群头像',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
file: 'file:///path/to/avatar.png',
|
||||
},
|
||||
notes: '示例路径位于 SnowLuma 服务端,须替换为该服务可读取的实际文件。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '设置群头像',
|
||||
params: { file: f.image() },
|
||||
run: async (p, ctx) => {
|
||||
|
||||
@@ -46,9 +46,70 @@ const albumCoverSchema = {
|
||||
required: ['type', 'image'],
|
||||
};
|
||||
|
||||
// Media-list entries are passed through rather than normalized like covers.
|
||||
// Missing fields can be absent; decoded undefined values become null.
|
||||
const mediaUrlSchema = {
|
||||
type: ['object', 'null'],
|
||||
properties: {
|
||||
url: { type: ['string', 'null'] },
|
||||
width: { type: ['integer', 'null'] },
|
||||
height: { type: ['integer', 'null'] },
|
||||
},
|
||||
};
|
||||
const mediaPhotoUrlsSchema = {
|
||||
type: ['array', 'null'],
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: { spec: { type: ['integer', 'null'] }, url: mediaUrlSchema },
|
||||
},
|
||||
};
|
||||
const mediaImageSchema = {
|
||||
type: ['object', 'null'],
|
||||
properties: {
|
||||
name: { type: ['string', 'null'] },
|
||||
sloc: { type: ['string', 'null'] },
|
||||
lloc: { type: ['string', 'null'], description: '图片长定位标识,供评论、删除等操作使用' },
|
||||
photoUrls: mediaPhotoUrlsSchema,
|
||||
defaultUrl: mediaUrlSchema,
|
||||
isGif: { type: ['boolean', 'null'] },
|
||||
hasRaw: { type: ['boolean', 'null'] },
|
||||
},
|
||||
};
|
||||
const albumMediaSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: ['integer', 'null'] },
|
||||
image: mediaImageSchema,
|
||||
video: {
|
||||
type: ['object', 'null'],
|
||||
properties: {
|
||||
id: { type: ['string', 'null'], description: '视频标识,供评论、删除等操作使用' },
|
||||
url: { type: ['string', 'null'] },
|
||||
cover: mediaImageSchema,
|
||||
width: { type: ['integer', 'null'] },
|
||||
height: { type: ['integer', 'null'] },
|
||||
videoTime: { type: ['string', 'null'], description: '时长,十进制字符串' },
|
||||
videoUrl: mediaPhotoUrlsSchema,
|
||||
},
|
||||
},
|
||||
uploader: { type: ['string', 'null'] },
|
||||
batchId: { type: ['string', 'null'], description: '上传批次,十进制字符串' },
|
||||
uploadTime: { type: ['string', 'null'], description: '上传时间,十进制字符串' },
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = [
|
||||
groupAction({
|
||||
name: 'get_group_album_list',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: '群相册列表数组,每项为一个相册的基本信息。',
|
||||
returnsSchema: {
|
||||
@@ -90,6 +151,16 @@ export const actions = [
|
||||
// get_qun_album_list — NapCat-compatible view of QQ NT's AlbumService.
|
||||
groupAction({
|
||||
name: 'get_qun_album_list',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
attach_info: '',
|
||||
},
|
||||
notes: '后续页原样传回上一页的 attach_info;has_more=false 表示结束。',
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: 'NapCat 风格的相册列表封套:{album_list, attach_info, has_more}。',
|
||||
returnsSchema: {
|
||||
@@ -153,6 +224,22 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'upload_image_to_qun_album',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
album_id: '<album-id>',
|
||||
album_name: '活动照片',
|
||||
file: 'file:///path/to/photo.png',
|
||||
},
|
||||
notes: 'album_id 来自 get_group_album_list;lloc 或视频 id、batch_id 来自 get_group_album_media_list。替换群号及对应资源标识。 示例路径位于 SnowLuma 服务端,须替换为该服务可读取的实际文件。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
params: {
|
||||
album_id: f.string({ allowEmpty: false }),
|
||||
album_name: f.string({ allowEmpty: false }),
|
||||
@@ -166,6 +253,18 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'upload_video_to_qun_album',
|
||||
examples: [
|
||||
{
|
||||
summary: '上传视频到群相册',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
album_id: '<album-id>',
|
||||
album_name: '活动视频',
|
||||
file: 'file:///path/to/video.mp4',
|
||||
},
|
||||
notes: 'album_id 来自 get_group_album_list;lloc 或视频 id、batch_id 来自 get_group_album_media_list。替换群号及对应资源标识。 示例路径位于 SnowLuma 服务端,须替换为该服务可读取的实际文件。',
|
||||
},
|
||||
],
|
||||
summary: '上传视频到群相册',
|
||||
returns: '{ id: string }',
|
||||
returnsSchema: {
|
||||
@@ -193,6 +292,17 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_album_media_list',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
album_id: '<album-id>',
|
||||
attach_info: '',
|
||||
},
|
||||
notes: 'album_id 来自 get_group_album_list;lloc 或视频 id、batch_id 来自 get_group_album_media_list。替换群号及对应资源标识。 下一页传回 nextAttachInfo。',
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns:
|
||||
'相册图片/视频列表及下一页分页游标;视频项包含 id、url、cover、尺寸、时长和多规格地址。',
|
||||
@@ -201,8 +311,8 @@ export const actions = [
|
||||
properties: {
|
||||
mediaList: {
|
||||
type: 'array',
|
||||
description: '相册媒体项列表(各项字段不固定)',
|
||||
items: { type: 'object' },
|
||||
description: '相册图片或视频,未返回的字段可省略或为 null',
|
||||
items: albumMediaSchema,
|
||||
},
|
||||
nextAttachInfo: { type: 'string', description: '下一页分页游标(空串表示无更多)' },
|
||||
},
|
||||
@@ -224,6 +334,58 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'do_group_album_comment',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
album_id: '<album-id>',
|
||||
lloc: '<media-lloc-or-video-id>',
|
||||
content: '拍得很好',
|
||||
},
|
||||
notes: 'album_id 来自 get_group_album_list;lloc 或视频 id、batch_id 来自 get_group_album_media_list。替换群号及对应资源标识。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uin: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['uin'],
|
||||
},
|
||||
content: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'integer',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['type', 'content'],
|
||||
},
|
||||
},
|
||||
time: {
|
||||
type: 'string',
|
||||
description: '十进制时间字符串',
|
||||
},
|
||||
clientKey: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['id', 'user', 'content', 'time', 'clientKey'],
|
||||
},
|
||||
params: {
|
||||
album_id: f.string({ allowEmpty: false }),
|
||||
lloc: f.string({ allowEmpty: false }),
|
||||
@@ -242,6 +404,28 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'set_group_album_media_like',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
album_id: '<album-id>',
|
||||
batch_id: '1700000000',
|
||||
},
|
||||
notes: 'album_id 来自 get_group_album_list;lloc 或视频 id、batch_id 来自 get_group_album_media_list。替换群号及对应资源标识。 batch_id 必须是实际批次的十进制字符串。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
status: {
|
||||
type: ['integer', 'null'],
|
||||
},
|
||||
},
|
||||
},
|
||||
params: {
|
||||
album_id: f.string({ allowEmpty: false }),
|
||||
batch_id: f.string({ allowEmpty: false }),
|
||||
@@ -262,6 +446,28 @@ export const actions = [
|
||||
// 取消点赞群相册媒体
|
||||
groupAction({
|
||||
name: 'cancel_group_album_media_like',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
album_id: '<album-id>',
|
||||
batch_id: '1700000000',
|
||||
},
|
||||
notes: 'album_id 来自 get_group_album_list;lloc 或视频 id、batch_id 来自 get_group_album_media_list。替换群号及对应资源标识。 batch_id 必须是实际批次的十进制字符串。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
status: {
|
||||
type: ['integer', 'null'],
|
||||
},
|
||||
},
|
||||
},
|
||||
params: {
|
||||
album_id: f.string({ allowEmpty: false }),
|
||||
batch_id: f.string({ allowEmpty: false }),
|
||||
@@ -281,6 +487,26 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'del_group_album_media',
|
||||
examples: [
|
||||
{
|
||||
summary: '删除群相册图片或视频',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
album_id: '<album-id>',
|
||||
lloc: '<media-lloc-or-video-id>',
|
||||
},
|
||||
notes: 'album_id 来自 get_group_album_list;lloc 或视频 id、batch_id 来自 get_group_album_media_list。替换群号及对应资源标识。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
const: true,
|
||||
},
|
||||
},
|
||||
required: ['success'],
|
||||
},
|
||||
summary: '删除群相册图片或视频',
|
||||
params: {
|
||||
album_id: f.string({ allowEmpty: false }),
|
||||
|
||||
@@ -20,6 +20,26 @@ function busidOr102(raw: unknown): number {
|
||||
export const actions = [
|
||||
groupAction({
|
||||
name: 'upload_group_file',
|
||||
examples: [
|
||||
{
|
||||
summary: '上传群文件',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
file: 'file:///path/to/report.pdf',
|
||||
name: '报告.pdf',
|
||||
},
|
||||
notes: '示例路径位于 SnowLuma 服务端,须替换为该服务可读取的实际文件。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_id: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['file_id'],
|
||||
},
|
||||
summary: '上传群文件',
|
||||
returns: '{ file_id: string }',
|
||||
// `folder` / `folder_id` are two aliases for one target dir; the first
|
||||
@@ -47,6 +67,26 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'upload_private_file',
|
||||
examples: [
|
||||
{
|
||||
summary: '上传私聊文件',
|
||||
params: {
|
||||
user_id: 10001,
|
||||
file: 'file:///path/to/report.pdf',
|
||||
name: '报告.pdf',
|
||||
},
|
||||
notes: '示例路径位于 SnowLuma 服务端,须替换为该服务可读取的实际文件。 默认上传并发送给该好友。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_id: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['file_id'],
|
||||
},
|
||||
summary: '上传私聊文件',
|
||||
returns: '{ file_id: string }',
|
||||
params: {
|
||||
@@ -101,6 +141,16 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_file_url',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群文件下载链接',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
file_id: '<file-id-from-get_group_root_files>',
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群文件下载链接',
|
||||
readOnly: true,
|
||||
returns: '群文件下载链接。',
|
||||
@@ -125,6 +175,15 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_root_files',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群根目录文件列表',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群根目录文件列表',
|
||||
readOnly: true,
|
||||
returns: '群文件系统信息(文件与文件夹列表)。',
|
||||
@@ -180,6 +239,16 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_files_by_folder',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群子目录文件列表',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
folder_id: '<folder-id-from-get_group_root_files>',
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群子目录文件列表',
|
||||
readOnly: true,
|
||||
returns: '群文件系统信息(文件与文件夹列表)。',
|
||||
@@ -238,6 +307,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'delete_group_file',
|
||||
examples: [
|
||||
{
|
||||
summary: '删除群文件',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
file_id: '<file-id-from-get_group_root_files>',
|
||||
},
|
||||
notes: '替换群文件列表中的实际 file_id;需要对应文件操作权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '删除群文件',
|
||||
params: { file_id: f.fileId() },
|
||||
run: async (p, ctx) => {
|
||||
@@ -248,6 +331,22 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'move_group_file',
|
||||
examples: [
|
||||
{
|
||||
summary: '移动群文件',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
file_id: '<file-id>',
|
||||
parent_directory: '/',
|
||||
target_directory: '<target-folder-id>',
|
||||
},
|
||||
notes: '目录和文件标识均来自该群文件列表。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '移动群文件',
|
||||
params: {
|
||||
file_id: f.fileId(),
|
||||
@@ -269,6 +368,27 @@ export const actions = [
|
||||
// (文件当前所在目录)+ new_name。SnowLuma 的 file_id 即原始 fileId,无需 UUID 解码。
|
||||
groupAction({
|
||||
name: 'rename_group_file',
|
||||
examples: [
|
||||
{
|
||||
summary: '重命名群文件',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
file_id: '<file-id>',
|
||||
current_parent_directory: '/',
|
||||
new_name: '新文件名.pdf',
|
||||
},
|
||||
notes: 'file_id 来自群文件列表,目录必须是文件当前所在目录。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: {
|
||||
const: true,
|
||||
},
|
||||
},
|
||||
required: ['ok'],
|
||||
},
|
||||
summary: '重命名群文件',
|
||||
params: {
|
||||
file_id: f.fileId(),
|
||||
@@ -290,6 +410,76 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'create_group_file_folder',
|
||||
examples: [
|
||||
{
|
||||
summary: '创建群文件夹',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
name: '共享资料',
|
||||
parent_id: '/',
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
retCode: {
|
||||
const: 0,
|
||||
},
|
||||
retMsg: {
|
||||
const: 'success',
|
||||
},
|
||||
},
|
||||
required: ['retCode', 'retMsg'],
|
||||
},
|
||||
groupItem: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
folderInfo: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
folderId: {
|
||||
type: 'string',
|
||||
},
|
||||
folderName: {
|
||||
type: 'string',
|
||||
},
|
||||
folderPath: {
|
||||
type: 'string',
|
||||
},
|
||||
createTime: {
|
||||
type: 'integer',
|
||||
},
|
||||
modifyTime: {
|
||||
type: 'integer',
|
||||
},
|
||||
createUin: {
|
||||
type: 'integer',
|
||||
},
|
||||
modifyUin: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'folderId',
|
||||
'folderName',
|
||||
'folderPath',
|
||||
'createTime',
|
||||
'modifyTime',
|
||||
'createUin',
|
||||
'modifyUin',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['folderInfo'],
|
||||
},
|
||||
},
|
||||
required: ['result', 'groupItem'],
|
||||
},
|
||||
summary: '创建群文件夹',
|
||||
// NapCat-aligned data so callers can read back the new folderId (#195):
|
||||
// { result, groupItem: { folderInfo: { folderId, folderName, … } } }.
|
||||
@@ -311,6 +501,20 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'delete_group_file_folder',
|
||||
examples: [
|
||||
{
|
||||
summary: '删除群文件夹',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
folder_id: '<folder-id-from-get_group_root_files>',
|
||||
},
|
||||
notes: '会删除指定群文件夹,确认资源后调用。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '删除群文件夹',
|
||||
params: { folder_id: f.string({ allowEmpty: false }) },
|
||||
run: async (p, ctx) => {
|
||||
@@ -321,6 +525,21 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'rename_group_file_folder',
|
||||
examples: [
|
||||
{
|
||||
summary: '重命名群文件夹',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
folder_id: '<folder-id>',
|
||||
new_folder_name: '课程资料',
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '重命名群文件夹',
|
||||
// new_folder_name / name are aliases; first non-empty wins and must be
|
||||
// non-empty (legacy `asString(new_folder_name) || asString(name)` + `!newName`).
|
||||
@@ -346,6 +565,16 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_private_file_url',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取私聊文件下载链接',
|
||||
params: {
|
||||
user_id: 10001,
|
||||
file_id: '<file-id-from-private-message>',
|
||||
},
|
||||
notes: '使用私聊文件消息段中的 file_id,必要时原样传 file_hash。',
|
||||
},
|
||||
],
|
||||
summary: '获取私聊文件下载链接',
|
||||
readOnly: true,
|
||||
returns: '私聊文件下载链接。',
|
||||
|
||||
@@ -33,6 +33,16 @@ export const groupInfoReturnsSchema = {
|
||||
export const actions = [
|
||||
groupAction({
|
||||
name: 'get_group_share_link',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群分享链接',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
need_short_url: true,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群分享链接',
|
||||
readOnly: true,
|
||||
params: {
|
||||
@@ -59,6 +69,14 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_group_list',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群列表',
|
||||
params: {
|
||||
no_cache: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
summary: '获取群列表',
|
||||
readOnly: true,
|
||||
returns: '群信息对象数组。',
|
||||
@@ -112,6 +130,15 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_info',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群信息',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群信息',
|
||||
readOnly: true,
|
||||
returns: '群信息对象。',
|
||||
@@ -144,6 +171,15 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_member_list',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群成员列表',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群成员列表',
|
||||
readOnly: true,
|
||||
returns: '群成员信息对象数组。',
|
||||
@@ -225,6 +261,16 @@ export const actions = [
|
||||
|
||||
groupUserAction({
|
||||
name: 'get_group_member_info',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群成员信息',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
user_id: 10001,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群成员信息',
|
||||
readOnly: true,
|
||||
returns: '群成员信息对象。',
|
||||
@@ -327,6 +373,164 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'get_group_honor_info',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群荣誉信息',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
type: 'all',
|
||||
},
|
||||
notes: '未请求的分类仍返回空数组;当前龙王可能为 null。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
group_id: {
|
||||
type: 'integer',
|
||||
},
|
||||
current_talkative: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: {
|
||||
type: ['number', 'string', 'null'],
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['user_id', 'nickname', 'avatar', 'description'],
|
||||
},
|
||||
{
|
||||
type: 'null',
|
||||
},
|
||||
],
|
||||
},
|
||||
talkative_list: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: {
|
||||
type: ['number', 'string', 'null'],
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['user_id', 'nickname', 'avatar', 'description'],
|
||||
},
|
||||
},
|
||||
performer_list: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: {
|
||||
type: ['number', 'string', 'null'],
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['user_id', 'nickname', 'avatar', 'description'],
|
||||
},
|
||||
},
|
||||
legend_list: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: {
|
||||
type: ['number', 'string', 'null'],
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['user_id', 'nickname', 'avatar', 'description'],
|
||||
},
|
||||
},
|
||||
emotion_list: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: {
|
||||
type: ['number', 'string', 'null'],
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['user_id', 'nickname', 'avatar', 'description'],
|
||||
},
|
||||
},
|
||||
strong_newbie_list: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: {
|
||||
type: ['number', 'string', 'null'],
|
||||
},
|
||||
nickname: {
|
||||
type: 'string',
|
||||
},
|
||||
avatar: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['user_id', 'nickname', 'avatar', 'description'],
|
||||
},
|
||||
description: '当前实现始终为空数组',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'group_id',
|
||||
'current_talkative',
|
||||
'talkative_list',
|
||||
'performer_list',
|
||||
'legend_list',
|
||||
'emotion_list',
|
||||
'strong_newbie_list',
|
||||
],
|
||||
},
|
||||
summary: '获取群荣誉信息',
|
||||
readOnly: true,
|
||||
params: { type: f.string().optional() },
|
||||
@@ -360,6 +564,17 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_group_system_msg',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取群系统消息',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
only_pending: true,
|
||||
count: 20,
|
||||
},
|
||||
notes: '将 group_id 替换为当前账号可访问的群号。',
|
||||
},
|
||||
],
|
||||
summary: '获取群系统消息',
|
||||
readOnly: true,
|
||||
returns: '群系统消息数组,可按群号或未处理状态过滤。',
|
||||
|
||||
@@ -5,6 +5,12 @@ import { okResponse } from '../types';
|
||||
export const actions = [
|
||||
defineAction({
|
||||
name: 'get_login_info',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: '当前登录账号的 QQ 号与昵称。',
|
||||
returnsSchema: {
|
||||
@@ -24,6 +30,12 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_status',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns:
|
||||
'运行状态。`online` 表示账号在线;`good` 表示已确认的收发链路健康状态;`time` 为当前账号服务实例运行秒数,与 #sl 同源。',
|
||||
@@ -55,6 +67,12 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_version_info',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: '实现与协议版本信息。',
|
||||
returnsSchema: {
|
||||
@@ -78,6 +96,12 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'can_send_image',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: '能力查询结果。',
|
||||
returnsSchema: {
|
||||
@@ -93,6 +117,12 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'can_send_record',
|
||||
examples: [
|
||||
{
|
||||
summary: '基本调用',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: '能力查询结果。',
|
||||
returnsSchema: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@snowluma/common/logger';
|
||||
import type { JsonObject, JsonValue } from '../types';
|
||||
import { defineAction, groupAction, f, type Field } from '../action-kit';
|
||||
import { RETCODE, failedResponse, okResponse } from '../types';
|
||||
import { messageResultSchema, messageIdSchema, messageDataSchema } from '../action-result-schemas';
|
||||
|
||||
/** send_msg only (#426). Some clients send `group_id: ""` on private chats.
|
||||
* Treat that exact empty string as absent. Do not change `f.groupId()`. */
|
||||
@@ -23,6 +24,17 @@ const log = createLogger('OneBot');
|
||||
export const actions = [
|
||||
defineAction({
|
||||
name: 'send_custom_face',
|
||||
examples: [
|
||||
{
|
||||
summary: '发送账号收藏表情(emoji_id 或 MD5),保留表情显示样式',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
emoji_id: '<emoji-id-from-fetch_custom_face_detail>',
|
||||
},
|
||||
notes: '群聊和私聊目标只能提供一个;emoji_id 来自 fetch_custom_face_detail。',
|
||||
},
|
||||
],
|
||||
returnsSchema: messageResultSchema,
|
||||
summary: '发送账号收藏表情(emoji_id 或 MD5),保留表情显示样式',
|
||||
returns: '{ message_id: number }',
|
||||
params: {
|
||||
@@ -68,6 +80,18 @@ export const actions = [
|
||||
// validated by the spec; message_type is left lenient for parity.
|
||||
defineAction({
|
||||
name: 'send_msg',
|
||||
examples: [
|
||||
{
|
||||
summary: '发送消息(按 message_type/群号 自动路由群聊或私聊)',
|
||||
params: {
|
||||
message_type: 'group',
|
||||
group_id: 20001,
|
||||
message: '你好',
|
||||
},
|
||||
notes: '必须提供对应目标;私聊使用 message_type=private 和 user_id。',
|
||||
},
|
||||
],
|
||||
returnsSchema: messageResultSchema,
|
||||
summary: '发送消息(按 message_type/群号 自动路由群聊或私聊)',
|
||||
returns: '{ message_id: number }',
|
||||
params: {
|
||||
@@ -109,6 +133,17 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'send_private_msg',
|
||||
examples: [
|
||||
{
|
||||
summary: '发送私聊消息',
|
||||
params: {
|
||||
user_id: 10001,
|
||||
message: '你好',
|
||||
},
|
||||
notes: '替换目标好友 QQ 号;临时会话需另传来源 group_id。',
|
||||
},
|
||||
],
|
||||
returnsSchema: messageResultSchema,
|
||||
summary: '发送私聊消息',
|
||||
returns: '{ message_id: number }',
|
||||
params: {
|
||||
@@ -135,6 +170,24 @@ export const actions = [
|
||||
|
||||
groupAction({
|
||||
name: 'send_group_msg',
|
||||
examples: [
|
||||
{
|
||||
summary: '发送群消息',
|
||||
params: {
|
||||
group_id: 20001,
|
||||
message: [
|
||||
{
|
||||
type: 'text',
|
||||
data: {
|
||||
text: '你好',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
notes: '替换目标群号;成功返回的 message_id 可为负值。',
|
||||
},
|
||||
],
|
||||
returnsSchema: messageResultSchema,
|
||||
summary: '发送群消息',
|
||||
returns: '{ message_id: number }',
|
||||
params: { message: f.message(), auto_escape: f.bool().default(false) },
|
||||
@@ -146,6 +199,28 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_msg',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取消息',
|
||||
params: {
|
||||
message_id: -123456,
|
||||
},
|
||||
notes: '使用消息事件或发送结果中的真实 message_id,负值合法;消息须在当前账号的消息存储中可查询。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
allOf: [
|
||||
messageDataSchema,
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
real_id: messageIdSchema,
|
||||
},
|
||||
required: ['real_id'],
|
||||
},
|
||||
],
|
||||
description: '消息事件去除 post_type/self_id 并增加 real_id;群聊和私聊字段有所不同',
|
||||
},
|
||||
summary: '获取消息',
|
||||
readOnly: true,
|
||||
returns:
|
||||
@@ -170,6 +245,19 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'delete_msg',
|
||||
examples: [
|
||||
{
|
||||
summary: '撤回消息',
|
||||
params: {
|
||||
message_id: -123456,
|
||||
},
|
||||
notes: '使用消息事件或发送结果中的真实 message_id,负值合法;消息须在当前账号的消息存储中可查询。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '撤回消息',
|
||||
params: { message_id: f.messageId() },
|
||||
run: async (p, ctx) => {
|
||||
|
||||
@@ -41,6 +41,16 @@ export const actions = [
|
||||
// 复用 qzone.qq.com 的 cookie/g_tk 基建(与群相册同源),纯 web,无 trpc。
|
||||
defineAction({
|
||||
name: 'get_qzone_msg_list',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取 QQ 空间说说列表(默认机器人自己的空间)',
|
||||
params: {
|
||||
pos: 0,
|
||||
num: 20,
|
||||
},
|
||||
notes: '默认读取当前账号;指定 target_uin 时需有访问权限。',
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: '说说列表对象,含说说总数与本页说说数组。',
|
||||
returnsSchema: {
|
||||
@@ -87,6 +97,17 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'get_qzone_msg_detail',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取 QQ 空间说说详情与一页评论(含服务端内联回复,需有访问权限)',
|
||||
params: {
|
||||
tid: '<tid-from-get_qzone_msg_list>',
|
||||
pos: 0,
|
||||
num: 20,
|
||||
},
|
||||
notes: 'tid 来自 get_qzone_msg_list;操作需有该说说的访问或管理权限。 replies 仅为服务端内联回复,不保证完整。',
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
summary: '获取 QQ 空间说说详情与一页评论(含服务端内联回复,需有访问权限)',
|
||||
returns:
|
||||
@@ -139,6 +160,17 @@ export const actions = [
|
||||
// 每条 feed 含结构化字段 + 预渲染 html 原样透传(深度解析交由调用方)。
|
||||
defineAction({
|
||||
name: 'get_qzone_feeds',
|
||||
examples: [
|
||||
{
|
||||
summary:
|
||||
'获取 QQ 空间好友动态(feed);page_num 仅首页可靠,深翻页需时间游标(暂未实现)',
|
||||
params: {
|
||||
page_num: 1,
|
||||
count: 10,
|
||||
},
|
||||
notes: '目前仅首页可靠,不应假设任意页码均能完整翻页。',
|
||||
},
|
||||
],
|
||||
readOnly: true,
|
||||
returns: '好友动态对象,含本页 feed 数组与是否有更多页。',
|
||||
returnsSchema: {
|
||||
@@ -187,6 +219,29 @@ export const actions = [
|
||||
// 注意:发说说为主动行为,高频会被 Qzone 风控,调用方需自行限流。
|
||||
defineAction({
|
||||
name: 'send_qzone_msg',
|
||||
examples: [
|
||||
{
|
||||
summary:
|
||||
'发表说说(QQ 空间,支持纯文字或带图;传 images 自动上传;可设置查看权限)',
|
||||
params: {
|
||||
content: '今天的学习记录',
|
||||
ugc_right: 64,
|
||||
},
|
||||
notes: '会发布说说;ugc_right=64 仅自己可见。16/128 还必须提供 target_uins。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tid: {
|
||||
type: 'string',
|
||||
},
|
||||
time: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['tid', 'time'],
|
||||
},
|
||||
summary: '发表说说(QQ 空间,支持纯文字或带图;传 images 自动上传;可设置查看权限)',
|
||||
params: {
|
||||
content: f.string({ allowEmpty: false }).describe('说说正文'),
|
||||
@@ -241,6 +296,19 @@ export const actions = [
|
||||
// delete_qzone_msg — 删除机器人自己空间的一条说说(按 tid)。写操作。
|
||||
defineAction({
|
||||
name: 'delete_qzone_msg',
|
||||
examples: [
|
||||
{
|
||||
summary: '删除一条说说(QQ 空间,按 tid)',
|
||||
params: {
|
||||
tid: '<tid-from-get_qzone_msg_list>',
|
||||
},
|
||||
notes: 'tid 来自 get_qzone_msg_list;操作需有该说说的访问或管理权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '删除一条说说(QQ 空间,按 tid)',
|
||||
params: {
|
||||
tid: f
|
||||
@@ -257,6 +325,17 @@ export const actions = [
|
||||
// 写操作;ugc_right/target_uins 语义与 send_qzone_msg 一致。
|
||||
defineAction({
|
||||
name: 'set_qzone_msg_right',
|
||||
examples: [
|
||||
{
|
||||
summary: '修改一条已发说说的查看权限(QQ 空间,按 tid)',
|
||||
params: {
|
||||
tid: '<tid-from-get_qzone_msg_list>',
|
||||
ugc_right: 16,
|
||||
target_uins: [10001],
|
||||
},
|
||||
notes: 'tid 来自 get_qzone_msg_list;操作需有该说说的访问或管理权限。 16 表示仅所选好友可见。',
|
||||
},
|
||||
],
|
||||
returns: '更新后的权限对象。',
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
@@ -304,6 +383,20 @@ export const actions = [
|
||||
// 传真实值更可靠;不传按 0。写操作;高频点赞会被 Qzone 风控,调用方需限流。
|
||||
defineAction({
|
||||
name: 'like_qzone',
|
||||
examples: [
|
||||
{
|
||||
summary: '给一条说说点赞(QQ 空间)',
|
||||
params: {
|
||||
tid: '<tid-from-get_qzone_msg_list>',
|
||||
target_uin: 10001,
|
||||
},
|
||||
notes: 'tid 来自 get_qzone_msg_list;操作需有该说说的访问或管理权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '给一条说说点赞(QQ 空间)',
|
||||
params: {
|
||||
tid: f.string({ allowEmpty: false }).describe('说说 tid'),
|
||||
@@ -324,6 +417,20 @@ export const actions = [
|
||||
// 注意:取消赞端点(internal_unlike_app)暂未经真机核实,待抓包确认。
|
||||
defineAction({
|
||||
name: 'unlike_qzone',
|
||||
examples: [
|
||||
{
|
||||
summary: '取消对一条说说的点赞(QQ 空间;取消赞端点待真机核实)',
|
||||
params: {
|
||||
tid: '<tid-from-get_qzone_msg_list>',
|
||||
target_uin: 10001,
|
||||
},
|
||||
notes: 'tid 来自 get_qzone_msg_list;操作需有该说说的访问或管理权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '取消对一条说说的点赞(QQ 空间;取消赞端点待真机核实)',
|
||||
params: {
|
||||
tid: f.string({ allowEmpty: false }).describe('说说 tid'),
|
||||
@@ -345,6 +452,27 @@ export const actions = [
|
||||
// 写操作;高频评论会被 Qzone 风控,调用方需限流。
|
||||
defineAction({
|
||||
name: 'comment_qzone',
|
||||
examples: [
|
||||
{
|
||||
summary: '评论一条说说(QQ 空间,支持纯文字或带图;传 images 自动上传)',
|
||||
params: {
|
||||
tid: '<tid-from-get_qzone_msg_list>',
|
||||
target_uin: 10001,
|
||||
content: '感谢分享',
|
||||
},
|
||||
notes: 'tid 来自 get_qzone_msg_list;操作需有该说说的访问或管理权限。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
comment_id: {
|
||||
type: 'string',
|
||||
description: '服务端未提供评论 ID 时为空串',
|
||||
},
|
||||
},
|
||||
required: ['comment_id'],
|
||||
},
|
||||
summary: '评论一条说说(QQ 空间,支持纯文字或带图;传 images 自动上传)',
|
||||
params: {
|
||||
tid: f.string({ allowEmpty: false }).describe('说说 tid'),
|
||||
@@ -377,6 +505,21 @@ export const actions = [
|
||||
// enable=true 拉黑,enable=false 解除拉黑。
|
||||
defineAction({
|
||||
name: 'set_qzone_ban',
|
||||
examples: [
|
||||
{
|
||||
summary:
|
||||
'拉黑或解除拉黑某人(修改机器人自身 QQ 空间黑名单;enable=true 拉黑,false 解除)',
|
||||
params: {
|
||||
user_id: 10001,
|
||||
enable: true,
|
||||
},
|
||||
notes: '修改当前账号空间黑名单。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '拉黑或解除拉黑某人(修改机器人自身 QQ 空间黑名单;enable=true 拉黑,false 解除)',
|
||||
params: {
|
||||
user_id: f.userId().describe('目标 QQ 号'),
|
||||
|
||||
@@ -4,6 +4,20 @@ import { okResponse } from '../types';
|
||||
export const actions = [
|
||||
defineAction({
|
||||
name: 'set_friend_add_request',
|
||||
examples: [
|
||||
{
|
||||
summary: '处理好友添加请求',
|
||||
params: {
|
||||
flag: '<flag-from-friend-request-event>',
|
||||
approve: true,
|
||||
},
|
||||
notes: 'flag 必须原样使用当前账号好友请求事件的 flag,不能根据 QQ 号构造。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '处理好友添加请求',
|
||||
params: {
|
||||
flag: f.string({ allowEmpty: false }),
|
||||
@@ -17,6 +31,21 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'set_group_add_request',
|
||||
examples: [
|
||||
{
|
||||
summary: '处理加群请求',
|
||||
params: {
|
||||
flag: '<flag-from-group-request-event>',
|
||||
sub_type: 'add',
|
||||
approve: true,
|
||||
},
|
||||
notes: 'flag 和 sub_type 原样取自同一条群请求事件;邀请事件使用 invite。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'null',
|
||||
description: '成功时 data 为 null;失败另见响应的 status、retcode 和 wording',
|
||||
},
|
||||
summary: '处理加群请求',
|
||||
params: {
|
||||
// Opaque handle from the original request event; callers must echo it.
|
||||
|
||||
@@ -28,6 +28,89 @@ import { okResponse, type ApiResponse, type JsonObject, type JsonValue } from '.
|
||||
import { type StreamSink, StreamStatus } from '../streaming';
|
||||
import { STREAM_ROOT, ensureStreamDirectory, registerActiveStreamItem } from '../stream-storage';
|
||||
|
||||
const downloadFrameSchema = {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
const: 'stream',
|
||||
},
|
||||
data_type: {
|
||||
const: 'file_info',
|
||||
},
|
||||
file_name: {
|
||||
type: 'string',
|
||||
},
|
||||
file_size: {
|
||||
type: 'integer',
|
||||
description: '字节数,0 可表示未知',
|
||||
},
|
||||
chunk_size: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['type', 'data_type', 'file_name', 'file_size', 'chunk_size'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
const: 'stream',
|
||||
},
|
||||
data_type: {
|
||||
const: 'file_chunk',
|
||||
},
|
||||
index: {
|
||||
type: 'integer',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
description: '该分块的 Base64 数据',
|
||||
},
|
||||
size: {
|
||||
type: 'integer',
|
||||
},
|
||||
progress: {
|
||||
type: 'number',
|
||||
minimum: 0,
|
||||
maximum: 100,
|
||||
},
|
||||
base64_size: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['type', 'data_type', 'index', 'data', 'size', 'progress', 'base64_size'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
const: 'response',
|
||||
},
|
||||
data_type: {
|
||||
const: 'file_complete',
|
||||
},
|
||||
file_name: {
|
||||
type: 'string',
|
||||
},
|
||||
total_chunks: {
|
||||
type: 'integer',
|
||||
},
|
||||
total_bytes: {
|
||||
type: 'integer',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['type', 'data_type', 'file_name', 'total_chunks', 'total_bytes', 'message'],
|
||||
},
|
||||
],
|
||||
description:
|
||||
'每个成功流式帧的 data;失败帧另由 status/retcode/wording 与 type=error 表示,不是 MCP 保存文件后的工具结果',
|
||||
};
|
||||
|
||||
const DEFAULT_CHUNK_BYTES = 64 * 1024;
|
||||
const MAX_CHUNK_REQUEST_BYTES = 16 * 1024 * 1024; // cap the client-requested chunk size
|
||||
const MAX_DOWNLOAD_BYTES = 4 * 1024 * 1024 * 1024; // 4 GiB total
|
||||
@@ -542,6 +625,17 @@ const downloadParams = {
|
||||
|
||||
const downloadFileStream = defineStreamAction({
|
||||
name: 'download_file_stream',
|
||||
examples: [
|
||||
{
|
||||
summary: '以流式方式下载文件(stream 目录本地文件 / URL / 缓存媒体)',
|
||||
params: {
|
||||
file: 'https://example.com/report.pdf',
|
||||
chunk_size: 65536,
|
||||
},
|
||||
notes: '替换实际公开可访问地址;经 invoke_action 调用,文件保存到 MCP 下载目录。',
|
||||
},
|
||||
],
|
||||
returnsSchema: downloadFrameSchema,
|
||||
summary: '以流式方式下载文件(stream 目录本地文件 / URL / 缓存媒体)',
|
||||
returns: '流式帧:file_info → file_chunk* → file_complete',
|
||||
params: downloadParams,
|
||||
@@ -550,6 +644,17 @@ const downloadFileStream = defineStreamAction({
|
||||
|
||||
const downloadFileImageStream = defineStreamAction({
|
||||
name: 'download_file_image_stream',
|
||||
examples: [
|
||||
{
|
||||
summary: '以流式方式下载图片(缓存图片 id / URL / stream 目录本地文件)',
|
||||
params: {
|
||||
file_id: '<image-file-from-message>',
|
||||
chunk_size: 65536,
|
||||
},
|
||||
notes: 'file_id 来自当前账号图片消息;经 invoke_action 调用。',
|
||||
},
|
||||
],
|
||||
returnsSchema: downloadFrameSchema,
|
||||
summary: '以流式方式下载图片(缓存图片 id / URL / stream 目录本地文件)',
|
||||
returns: '流式帧:file_info → file_chunk* → file_complete',
|
||||
params: downloadParams,
|
||||
@@ -558,6 +663,17 @@ const downloadFileImageStream = defineStreamAction({
|
||||
|
||||
const downloadFileRecordStream = defineStreamAction({
|
||||
name: 'download_file_record_stream',
|
||||
examples: [
|
||||
{
|
||||
summary: '以流式方式下载语音(缓存语音 id / URL / stream 目录本地文件)',
|
||||
params: {
|
||||
file_id: '<record-file-from-message>',
|
||||
chunk_size: 65536,
|
||||
},
|
||||
notes: 'file_id 来自当前账号语音消息;经 invoke_action 调用。',
|
||||
},
|
||||
],
|
||||
returnsSchema: downloadFrameSchema,
|
||||
summary: '以流式方式下载语音(缓存语音 id / URL / stream 目录本地文件)',
|
||||
returns: '流式帧:file_info → file_chunk* → file_complete',
|
||||
params: downloadParams,
|
||||
@@ -567,6 +683,50 @@ const downloadFileRecordStream = defineStreamAction({
|
||||
|
||||
const testDownloadStream = defineStreamAction({
|
||||
name: 'test_download_stream',
|
||||
examples: [
|
||||
{
|
||||
summary: '测试下载流(推送 10 个数据帧,验证流式传输,不触达 QQ)',
|
||||
params: {
|
||||
error: false,
|
||||
},
|
||||
notes: '经 invoke_action 调用,仅测试传输,不访问 QQ。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
const: 'stream',
|
||||
},
|
||||
data_type: {
|
||||
const: 'data_chunk',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['type', 'data_type', 'data'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
const: 'response',
|
||||
},
|
||||
data_type: {
|
||||
const: 'data_complete',
|
||||
},
|
||||
data: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['type', 'data_type', 'data'],
|
||||
},
|
||||
],
|
||||
description: '成功时 10 个 data_chunk 后接 data_complete;error=true 时最终为失败帧',
|
||||
},
|
||||
summary: '测试下载流(推送 10 个数据帧,验证流式传输,不触达 QQ)',
|
||||
returns: '流式帧:data_chunk*10 → data_complete(error=true 时以 error 帧结束)',
|
||||
params: { error: f.bool().default(false).describe('是否触发测试错误') },
|
||||
|
||||
@@ -457,6 +457,98 @@ type UploadParams = {
|
||||
|
||||
const uploadFileStream = defineStreamAction({
|
||||
name: 'upload_file_stream',
|
||||
examples: [
|
||||
{
|
||||
summary: '上传分块',
|
||||
params: {
|
||||
stream_id: 'example-upload-001',
|
||||
filename: 'hello.txt',
|
||||
total_chunks: 1,
|
||||
file_size: 5,
|
||||
chunk_index: 0,
|
||||
chunk_data: 'aGVsbG8=',
|
||||
},
|
||||
notes: '第 1 步:每次上传生成新的 stream_id;这里上传 hello 的唯一分块。通过 invoke_action 调用,分块确认不等于整文件完成。',
|
||||
},
|
||||
{
|
||||
summary: '完成同一上传',
|
||||
params: {
|
||||
stream_id: 'example-upload-001',
|
||||
is_complete: true,
|
||||
},
|
||||
notes: '第 2 步:等待全部分块确认后,用同一个 stream_id 单独请求完成;成功时返回 file_path。MCP 的 invoke_action 也可通过 execution.input_file 自动完成上传。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
const: 'stream',
|
||||
},
|
||||
stream_id: {
|
||||
type: 'string',
|
||||
},
|
||||
status: {
|
||||
enum: ['file_created', 'chunk_received'],
|
||||
},
|
||||
received_chunks: {
|
||||
type: 'integer',
|
||||
},
|
||||
total_chunks: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['type', 'stream_id', 'status', 'received_chunks', 'total_chunks'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
const: 'response',
|
||||
},
|
||||
stream_id: {
|
||||
type: 'string',
|
||||
},
|
||||
status: {
|
||||
const: 'file_complete',
|
||||
},
|
||||
received_chunks: {
|
||||
type: 'integer',
|
||||
},
|
||||
total_chunks: {
|
||||
type: 'integer',
|
||||
},
|
||||
file_path: {
|
||||
type: 'string',
|
||||
},
|
||||
file_name: {
|
||||
type: 'string',
|
||||
},
|
||||
file_size: {
|
||||
type: 'integer',
|
||||
},
|
||||
sha256: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'type',
|
||||
'stream_id',
|
||||
'status',
|
||||
'received_chunks',
|
||||
'total_chunks',
|
||||
'file_path',
|
||||
'file_name',
|
||||
'file_size',
|
||||
'sha256',
|
||||
],
|
||||
},
|
||||
],
|
||||
description:
|
||||
'分块确认与完成帧的 data;reset 和其他失败由错误帧返回。必须等到 file_complete 才能使用 file_path',
|
||||
},
|
||||
summary: '以流式分块方式上传文件到机器人本地(返回可用于发送的本地路径)',
|
||||
returns:
|
||||
'流式帧:分块确认 type=stream、完成 type=response(含 file_path/file_name/file_size/sha256)',
|
||||
@@ -496,6 +588,31 @@ function cleanDir(dir: string): number {
|
||||
|
||||
const cleanStreamTempFile = defineAction({
|
||||
name: 'clean_stream_temp_file',
|
||||
examples: [
|
||||
{
|
||||
summary: '清理流式传输临时文件(仅清理 stream 上传/下载目录)',
|
||||
params: {},
|
||||
notes: '清理未使用的流式临时文件;不会清理正在传输的项目。',
|
||||
},
|
||||
],
|
||||
returnsSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
const: 'success',
|
||||
},
|
||||
removed: {
|
||||
type: 'integer',
|
||||
},
|
||||
freed_bytes: {
|
||||
type: 'integer',
|
||||
},
|
||||
skipped_active: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
required: ['message', 'removed', 'freed_bytes', 'skipped_active'],
|
||||
},
|
||||
summary: '清理流式传输临时文件(仅清理 stream 上传/下载目录)',
|
||||
returns: '{ message, removed }',
|
||||
params: {},
|
||||
|
||||
@@ -62,6 +62,14 @@ const faceSchema = {
|
||||
export const actions = [
|
||||
defineAction({
|
||||
name: 'fetch_sys_faces',
|
||||
examples: [
|
||||
{
|
||||
summary: '获取 QQ 系统表情目录',
|
||||
params: {
|
||||
refresh: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
summary: '获取 QQ 系统表情目录',
|
||||
readOnly: true,
|
||||
returns: '按分组返回完整的 QQ 系统表情映射。',
|
||||
@@ -96,6 +104,14 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'fetch_face_entity',
|
||||
examples: [
|
||||
{
|
||||
summary: '按编号查询 QQ 系统表情',
|
||||
params: {
|
||||
face_id: 14,
|
||||
},
|
||||
},
|
||||
],
|
||||
summary: '按编号查询 QQ 系统表情',
|
||||
readOnly: true,
|
||||
returns: '表情详情;编号不存在时返回 null。',
|
||||
@@ -112,6 +128,14 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'search_sys_faces',
|
||||
examples: [
|
||||
{
|
||||
summary: '搜索 QQ 系统表情',
|
||||
params: {
|
||||
query: '微笑',
|
||||
},
|
||||
},
|
||||
],
|
||||
summary: '搜索 QQ 系统表情',
|
||||
readOnly: true,
|
||||
returns: '匹配编号、名称、别名或分组名的表情列表。',
|
||||
@@ -131,6 +155,14 @@ export const actions = [
|
||||
|
||||
defineAction({
|
||||
name: 'fetch_super_face_id',
|
||||
examples: [
|
||||
{
|
||||
summary: '判断 QQ 系统表情是否使用超级表情格式',
|
||||
params: {
|
||||
face_id: 14,
|
||||
},
|
||||
},
|
||||
],
|
||||
summary: '判断 QQ 系统表情是否使用超级表情格式',
|
||||
readOnly: true,
|
||||
returns: '是否使用超级表情格式。',
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// declarative action and renders coherent metadata/markdown.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { collectActionDocs, renderActionDocsMarkdown } from '../src/action-docs';
|
||||
import { ACTION_REGISTRY } from '../src/actions';
|
||||
import { defineAction, defineStreamAction } from '../src/action-kit';
|
||||
import { okResponse } from '../src/types';
|
||||
|
||||
describe('action-docs', () => {
|
||||
const docs = collectActionDocs();
|
||||
@@ -75,6 +78,54 @@ describe('action-docs', () => {
|
||||
expect(md).toContain('# OneBot Actions');
|
||||
expect(md).toContain('`set_group_ban`');
|
||||
expect(md).toContain('| 参数 | 类型 | 必填 | 默认 | 说明 |');
|
||||
expect(md).toContain('"action": "set_group_ban"');
|
||||
expect(md).toContain('"duration": 600');
|
||||
});
|
||||
|
||||
it.each(ACTION_REGISTRY.actions)(
|
||||
'$canonical examples pass the real parameter parser',
|
||||
({ spec, doc }) => {
|
||||
expect(doc.returnsSchema).toBeTypeOf('object');
|
||||
expect(doc.examples?.length).toBeGreaterThan(0);
|
||||
|
||||
// Both built-in spec kinds expose a pure parser; do not call handlers
|
||||
// here, since examples include writes and process-lifecycle actions.
|
||||
if (!('parse' in spec) || typeof spec.parse !== 'function') {
|
||||
throw new Error(`${doc.name} has no pure parameter parser`);
|
||||
}
|
||||
|
||||
for (const example of doc.examples ?? []) {
|
||||
expect(example.summary).not.toBe('');
|
||||
const keys = new Set(doc.params.map((param) => param.name));
|
||||
|
||||
for (const key of Object.keys(example.params)) {
|
||||
expect(keys.has(key), `${doc.name} unknown example param ${key}`).toBe(true);
|
||||
}
|
||||
|
||||
expect(spec.parse(example.params), `${doc.name}: ${example.summary}`).toMatchObject(
|
||||
{ ok: true },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps documentation metadata optional for existing custom specs', () => {
|
||||
for (const define of [defineAction, defineStreamAction]) {
|
||||
const spec = define({ name: 'custom', params: {}, run: () => okResponse() });
|
||||
expect(spec.describe().examples).toBeUndefined();
|
||||
expect(spec.describe().returnsSchema).toBeUndefined();
|
||||
expect(spec.parse({})).toMatchObject({ ok: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('isolates collected examples and schemas from caller mutations', () => {
|
||||
const copy = collectActionDocs();
|
||||
const action = copy.find((doc) => doc.name === 'send_group_msg')!;
|
||||
action.examples![0].params.group_id = 99999;
|
||||
action.returnsSchema!.type = 'null';
|
||||
const fresh = collectActionDocs().find((doc) => doc.name === action.name)!;
|
||||
expect(fresh.examples![0].params.group_id).toBe(20001);
|
||||
expect(fresh.returnsSchema!.type).toBe('object');
|
||||
});
|
||||
|
||||
it('documents custom reply content on every forward entry point', () => {
|
||||
|
||||
@@ -403,7 +403,7 @@ describe('set_group_ban describe', () => {
|
||||
expect(doc.summary).toBe('禁言群成员(duration=0 解除)');
|
||||
expect(doc.readOnly).toBe(false);
|
||||
expect(doc.returns).toBeUndefined();
|
||||
expect(doc.returnsSchema).toBeUndefined();
|
||||
expect(doc.returnsSchema).toMatchObject({ type: 'null' });
|
||||
expect(doc.stream).toBeUndefined();
|
||||
expect(doc.invariants).toEqual([]);
|
||||
expect(doc.params).toEqual([
|
||||
|
||||
@@ -228,7 +228,7 @@ describe('set_friend_add_request describe', () => {
|
||||
expect(doc.summary).toBe('处理好友添加请求');
|
||||
expect(doc.readOnly).toBe(false);
|
||||
expect(doc.returns).toBeUndefined();
|
||||
expect(doc.returnsSchema).toBeUndefined();
|
||||
expect(doc.returnsSchema).toMatchObject({ type: 'null' });
|
||||
expect(doc.stream).toBeUndefined();
|
||||
expect(doc.invariants).toEqual([]);
|
||||
expect(doc.params).toEqual([
|
||||
@@ -526,7 +526,7 @@ describe('set_group_add_request describe', () => {
|
||||
expect(doc.summary).toBe('处理加群请求');
|
||||
expect(doc.readOnly).toBe(false);
|
||||
expect(doc.returns).toBeUndefined();
|
||||
expect(doc.returnsSchema).toBeUndefined();
|
||||
expect(doc.returnsSchema).toMatchObject({ type: 'null' });
|
||||
expect(doc.stream).toBeUndefined();
|
||||
expect(doc.invariants).toEqual([]);
|
||||
expect(doc.params).toEqual([
|
||||
|
||||
Reference in New Issue
Block a user