ugc-lens 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ugc-lens
2
2
 
3
- 面向中文社媒用户互动内容(评论、回复、弹幕等)的轻量分析 Agent。直接启动后用自然语言描述目标,它会选择工具导入数据、生成带原文引用的报告、连续追问或监听目录。以 npm 包形式提供命令行和代码调用,MCP 服务后续提供。
3
+ 面向中文社媒用户互动内容(评论、回复、弹幕等)的轻量分析 Agent。直接启动后用自然语言描述目标,它会选择工具导入数据、生成带原文引用的报告、连续追问或监听目录。以 npm 包形式提供命令行和代码调用,也提供 stdio MCP 服务供宿主调用。
4
4
 
5
5
  > 状态:0.0.x 试用版。目前只支持 JSON 文件,接口和报告格式还可能调整。
6
6
 
@@ -81,6 +81,49 @@ ugc-lens <命令> --help # 查看全部选项
81
81
  - 统计、去重、关键词、引用校验都由代码完成,模型只负责分块总结和合并,一个 10 万条评论的文件大约用 6~7 万 token。
82
82
  - `watch` 的任务状态存在 `.ugc-lens/tasks.json`,按 Ctrl+C 停止后再启动会继续没做完的任务。
83
83
 
84
+ ## MCP 接入
85
+
86
+ 先用 `ugc-lens init` 配置模型。MCP 由宿主启动,不需要自己开一个服务终端。Claude Code 中可以添加:
87
+
88
+ ```bash
89
+ claude mcp add --transport stdio ugc-lens -- npx -y ugc-lens@0.0.4 mcp --cwd /绝对路径/数据项目
90
+ ```
91
+
92
+ 测试本地源码时:
93
+
94
+ ```bash
95
+ claude mcp add --transport stdio ugc-lens -- node /绝对路径/ugc-lens/src/cli/index.ts mcp --cwd /绝对路径/数据项目
96
+ ```
97
+
98
+ 两种配置选一种。在 Claude Code 的 `/mcp` 中确认连接,然后直接说“分析 `/绝对路径/video_all.json`,重点看价格”,宿主会选择工具。`--cwd` 指定配置、缓存、报告和相对数据路径的基准目录;服务沿用环境变量、项目配置、全局配置的优先级。
99
+
100
+ 其他支持 stdio 的宿主可使用同等配置:
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "ugc-lens": {
106
+ "command": "npx",
107
+ "args": ["-y", "ugc-lens@0.0.4", "mcp", "--cwd", "/绝对路径/数据项目"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ | 工具 | 用途 |
114
+ |---|---|
115
+ | `load_dataset` | 导入 JSON,返回数据集 ID |
116
+ | `list_datasets` | 列出本次服务已导入的数据 |
117
+ | `get_dataset_stats` | 查看数据概况 |
118
+ | `search_feedback` | 按关键词、类型、点赞等筛选并分页 |
119
+ | `get_feedback` | 查看全文和回复链 |
120
+ | `analyze` | 自动导入并生成完整报告,返回正文和保存路径 |
121
+ | `ask` | 调用配置的模型回答,返回引用核对结果及追问 ID |
122
+
123
+ `analyze`、`ask` 可以传 `file` 自动导入,也可用 `dataset_id`;两者不能同时提供。`ask` 返回 `conversation_id`,继续追问时传回这个 ID 和新 `question`,可省略文件。数据集和对话只在当前服务进程内保留;报告和映射缓存保存在磁盘。MCP 本身不使用宿主的模型授权,识别结构和模型分析仍需要 ugc-lens 的模型配置;握手和工具发现不需要 key。
124
+
125
+ 技能指引位于 [skills/ugc-lens/SKILL.md](skills/ugc-lens/SKILL.md),随 npm 包提供,可按宿主的技能安装方式添加。代码调用者可从 `ugc-lens/mcp` 导入 `createMcpServer` 创建独立实例。
126
+
84
127
  ## 代码调用
85
128
 
86
129
  ```ts
package/dist/cli/index.js CHANGED
@@ -12,8 +12,10 @@ import { UsageError } from './common.js';
12
12
  import { runChat } from './chat.js';
13
13
  import { INIT_USAGE, runInit } from './init.js';
14
14
  import { runWatch, WATCH_USAGE } from './watch.js';
15
+ import { MCP_USAGE, runMcp } from './mcp.js';
15
16
  const COMMANDS = {
16
17
  init: { run: runInit, usage: INIT_USAGE },
18
+ mcp: { run: runMcp, usage: MCP_USAGE },
17
19
  analyze: { run: runAnalyze, usage: ANALYZE_USAGE },
18
20
  ask: { run: runAsk, usage: ASK_USAGE },
19
21
  watch: { run: runWatch, usage: WATCH_USAGE },
@@ -26,6 +28,7 @@ const MAIN_USAGE = `ugc-lens:分析评论、弹幕等用户互动内容
26
28
  ugc-lens analyze <文件> 生成 Markdown 分析报告
27
29
  ugc-lens ask <文件> <问题> [追问...] 追问,回答带 [ID] 引用
28
30
  ugc-lens watch <目录> 监听目录,新的 *_all.json 写完后自动分析
31
+ ugc-lens mcp [--cwd <目录>] 通过 stdio 向宿主提供 MCP 工具
29
32
  ugc-lens <命令> --help 查看命令的选项
30
33
  ugc-lens --version
31
34
 
@@ -0,0 +1,5 @@
1
+ export declare const MCP_USAGE = "\u7528\u6CD5\uFF1Augc-lens mcp [--cwd <\u76EE\u5F55>]\n\n\u901A\u8FC7 stdio \u63D0\u4F9B MCP \u5DE5\u5177\uFF0C\u7531 Claude Code \u7B49\u5BBF\u4E3B\u542F\u52A8\u3002\n --cwd <\u76EE\u5F55> \u914D\u7F6E\u3001\u7F13\u5B58\u3001\u62A5\u544A\u548C\u76F8\u5BF9\u6570\u636E\u8DEF\u5F84\u7684\u57FA\u51C6\u76EE\u5F55\uFF0C\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\n\n\u6807\u51C6\u8F93\u51FA\u4EC5\u7528\u4E8E MCP \u534F\u8BAE\uFF0C\u4E0D\u80FD\u4F5C\u4E3A\u4EA4\u4E92\u5BF9\u8BDD\u7EC8\u7AEF\u4F7F\u7528\u3002";
2
+ export declare function parseMcpArgs(argv: readonly string[]): {
3
+ cwd: string;
4
+ };
5
+ export declare function runMcp(argv: readonly string[]): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { UsageError } from './common.js';
5
+ export const MCP_USAGE = `用法:ugc-lens mcp [--cwd <目录>]
6
+
7
+ 通过 stdio 提供 MCP 工具,由 Claude Code 等宿主启动。
8
+ --cwd <目录> 配置、缓存、报告和相对数据路径的基准目录,默认当前目录
9
+
10
+ 标准输出仅用于 MCP 协议,不能作为交互对话终端使用。`;
11
+ export function parseMcpArgs(argv) {
12
+ const { values, positionals } = parseArgs({ args: [...argv], allowPositionals: true, options: { cwd: { type: 'string' } } });
13
+ if (positionals.length)
14
+ throw new UsageError('mcp 不接受位置参数,请用 --cwd 指定工作目录');
15
+ if (values.cwd !== undefined && !values.cwd.trim())
16
+ throw new UsageError('--cwd 需要目录路径');
17
+ return { cwd: resolve(values.cwd ?? process.cwd()) };
18
+ }
19
+ export async function runMcp(argv) {
20
+ const { cwd } = parseMcpArgs(argv);
21
+ try {
22
+ if (!(await stat(cwd)).isDirectory())
23
+ throw new Error('不是目录');
24
+ }
25
+ catch {
26
+ throw new UsageError(`MCP 工作目录不存在或不是目录:${cwd}`);
27
+ }
28
+ const [{ serveStdio }, { createMcpServer }] = await Promise.all([
29
+ import('@modelcontextprotocol/server/stdio'), import('../mcp/server.js'),
30
+ ]);
31
+ let finished;
32
+ const done = new Promise((resolve) => { finished = resolve; });
33
+ const handle = serveStdio(() => {
34
+ const server = createMcpServer({ cwd });
35
+ const onclose = server.server.onclose;
36
+ server.server.onclose = () => { onclose?.(); finished(); };
37
+ return server;
38
+ }, { onerror: (error) => console.error(`MCP:${error.message}`) });
39
+ const stop = () => {
40
+ void handle.close().then(finished, (error) => {
41
+ console.error(`MCP 关闭失败:${error instanceof Error ? error.message : String(error)}`);
42
+ process.exitCode = 1;
43
+ finished();
44
+ });
45
+ };
46
+ process.on('SIGINT', stop);
47
+ process.on('SIGTERM', stop);
48
+ // 没有握手就 EOF 时也应退出;有请求时由 SDK 取消并清理。
49
+ process.stdin.once('end', stop);
50
+ try {
51
+ await done;
52
+ }
53
+ finally {
54
+ await handle.close();
55
+ process.removeListener('SIGINT', stop);
56
+ process.removeListener('SIGTERM', stop);
57
+ process.stdin.removeListener('end', stop);
58
+ }
59
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** 代码 API。命令行和以后的 MCP 服务只通过这些函数使用核心功能 */
1
+ /** 代码 API。命令行和 MCP 服务只通过这些函数使用核心功能 */
2
2
  export { loadLlmConfig, ConfigError, type LlmConfig, type LlmConfigFile } from './config.js';
3
3
  export { createChatClient, LlmError, type ChatClient, type ChatClientOptions } from './llm/client.js';
4
4
  export { PROVIDERS, type ProviderId, type ProviderPreset, type Region } from './llm/providers.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** 代码 API。命令行和以后的 MCP 服务只通过这些函数使用核心功能 */
1
+ /** 代码 API。命令行和 MCP 服务只通过这些函数使用核心功能 */
2
2
  export { loadLlmConfig, ConfigError } from './config.js';
3
3
  export { createChatClient, LlmError } from './llm/client.js';
4
4
  export { PROVIDERS } from './llm/providers.js';
@@ -0,0 +1,14 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import type { AgentModel } from '../agent/model.js';
3
+ import { loadDataset } from '../ingest/index.js';
4
+ import { type AnalyzeOptions, type AnalyzeResult } from '../pipeline/analyze.js';
5
+ export interface McpServerOptions {
6
+ cwd?: string;
7
+ model?: AgentModel;
8
+ analysisModel?: AgentModel;
9
+ modelName?: string;
10
+ load?: typeof loadDataset;
11
+ analyze?: (options: AnalyzeOptions) => Promise<Pick<AnalyzeResult, 'markdown' | 'path' | 'calls' | 'usage'>>;
12
+ }
13
+ /** 每次创建独立状态;宿主直接调数据工具时,不需要再嵌套一个问答 Agent。 */
14
+ export declare function createMcpServer(options?: McpServerOptions): McpServer;
@@ -0,0 +1,156 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { fromJsonSchema, McpServer } from '@modelcontextprotocol/server';
5
+ import * as z from 'zod';
6
+ import { createLlmModel } from '../agent/llm_model.js';
7
+ import { askDataset } from '../ask/index.js';
8
+ import { loadLlmConfig } from '../config.js';
9
+ import { loadDataset } from '../ingest/index.js';
10
+ import { createChatClient } from '../llm/client.js';
11
+ import { analyzeDataset } from '../pipeline/analyze.js';
12
+ import { denyAll } from '../tools/approval.js';
13
+ import { createDataTools } from '../tools/data/index.js';
14
+ import { resolveDataset } from '../tools/data/shared.js';
15
+ import { defineTool } from '../tools/define.js';
16
+ const selector = {
17
+ file: z.string().min(1).optional().describe('JSON 文件路径,相对于服务工作目录;提供后自动导入'),
18
+ dataset_id: z.string().min(1).optional().describe('已导入的数据集 ID,不能与 file 同时提供;只有一个数据集时可省略'),
19
+ };
20
+ /** 每次创建独立状态;宿主直接调数据工具时,不需要再嵌套一个问答 Agent。 */
21
+ export function createMcpServer(options = {}) {
22
+ const cwd = resolve(options.cwd ?? process.cwd());
23
+ const lifetime = new AbortController();
24
+ const datasets = new Map();
25
+ const conversations = new Map();
26
+ const store = { list: () => [...datasets.values()], get: (id) => datasets.get(id) };
27
+ const version = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
28
+ const server = new McpServer({ name: 'ugc-lens', version }, {
29
+ instructions: '分析本地 JSON 评论、回复与弹幕。load_dataset 导入后可直接使用数据工具查询;analyze 生成报告;ask 使用配置的模型回答并返回 conversation_id,继续追问时传回该 ID。所有相对路径以服务 cwd 为准。',
30
+ });
31
+ let models;
32
+ const getModels = () => {
33
+ if (models)
34
+ return models;
35
+ if (options.model) {
36
+ models = { ask: options.model, analysis: options.analysisModel ?? options.model, name: options.modelName };
37
+ }
38
+ else {
39
+ const config = loadLlmConfig({}, cwd);
40
+ const client = createChatClient(config);
41
+ models = {
42
+ ask: createLlmModel(client, { temperature: 0.2 }),
43
+ analysis: options.analysisModel ?? createLlmModel(client, { temperature: 0, maxTokens: 4_000, disableThinking: true }),
44
+ name: config.model,
45
+ };
46
+ }
47
+ return models;
48
+ };
49
+ const importFile = async (file, signal) => {
50
+ const path = resolve(cwd, file);
51
+ const cached = store.list().find((dataset) => dataset.file_path === path);
52
+ if (cached)
53
+ return cached;
54
+ const dataset = await (options.load ?? loadDataset)(path, {
55
+ model: getModels().analysis, signal, cacheDir: join(cwd, '.ugc-lens', 'mappings'),
56
+ });
57
+ signal.throwIfAborted();
58
+ datasets.set(dataset.id, dataset);
59
+ return dataset;
60
+ };
61
+ const select = async (args, signal) => {
62
+ if (args.file && args.dataset_id)
63
+ throw new Error('file 和 dataset_id 只能提供一个');
64
+ return args.file ? importFile(args.file, signal) : resolveDataset(store, args.dataset_id);
65
+ };
66
+ const dataTools = createDataTools(store);
67
+ const tools = [
68
+ ...dataTools,
69
+ defineTool({
70
+ name: 'load_dataset', description: '导入本地 JSON 评论、回复或弹幕文件,返回 dataset_id。后续查询可复用,不需要反复发送原始文件。',
71
+ parameters: z.object({ file: z.string().min(1) }),
72
+ async execute({ file }, ctx) {
73
+ const dataset = await importFile(file, ctx.signal);
74
+ return JSON.stringify({ dataset_id: dataset.id, file: dataset.file_path, items: dataset.items.length, contents: dataset.contents.length });
75
+ },
76
+ }),
77
+ defineTool({
78
+ name: 'analyze', description: '对本地 JSON 文件或已导入数据生成完整的 Markdown 分析报告,返回报告正文和保存路径。需要配置 ugc-lens 模型。',
79
+ parameters: z.object({
80
+ ...selector,
81
+ directions: z.array(z.string().min(1)).min(1).optional().describe('关注方向;不传使用默认分析方向'),
82
+ max_total_chars: z.number().int().positive().optional().describe('发送给模型的评论总字数上限,默认 80000'),
83
+ }),
84
+ async execute({ directions, max_total_chars, ...args }, ctx) {
85
+ const dataset = await select(args, ctx.signal);
86
+ const models = getModels();
87
+ const result = await (options.analyze ?? analyzeDataset)({
88
+ dataset, model: models.analysis, modelName: models.name, cwd, directions,
89
+ maxTotalChars: max_total_chars, signal: ctx.signal,
90
+ });
91
+ ctx.signal.throwIfAborted();
92
+ return JSON.stringify({ dataset_id: dataset.id, path: result.path, markdown: result.markdown, calls: result.calls, usage: result.usage });
93
+ },
94
+ }),
95
+ defineTool({
96
+ name: 'ask', description: '使用 ugc-lens 配置的模型查询数据并回答,返回带 [ID] 的回答和引用核对结果。连续追问时传回 conversation_id;宿主自己分析时可直接调用数据工具。',
97
+ parameters: z.object({
98
+ ...selector,
99
+ question: z.string().trim().min(1),
100
+ conversation_id: z.string().min(1).optional().describe('上一轮返回的 ID;不填则创建独立对话'),
101
+ }),
102
+ async execute({ conversation_id, question, ...args }, ctx) {
103
+ const previous = conversation_id ? conversations.get(conversation_id) : undefined;
104
+ if (conversation_id && !previous)
105
+ throw new Error('找不到追问会话,请不传 conversation_id 开始新对话');
106
+ if (previous?.busy)
107
+ throw new Error('这段对话的上一轮尚未完成,请等待后重试');
108
+ if (previous)
109
+ previous.busy = true;
110
+ try {
111
+ const dataset = previous && !args.file && !args.dataset_id
112
+ ? resolveDataset(store, previous.dataset_id)
113
+ : await select(args, ctx.signal);
114
+ if (previous && previous.dataset_id !== dataset.id)
115
+ throw new Error('追问会话绑定了其他数据集,请不传 conversation_id 开始新对话');
116
+ const result = await askDataset({ model: getModels().ask, datasets: [dataset], question, history: previous?.history, signal: ctx.signal });
117
+ ctx.signal.throwIfAborted();
118
+ const id = conversation_id ?? randomUUID();
119
+ conversations.set(id, { dataset_id: dataset.id, history: result.run.messages, busy: false });
120
+ return JSON.stringify({
121
+ conversation_id: id, dataset_id: dataset.id, answer: result.answer, citations: result.citations,
122
+ stop_reason: result.run.stop_reason, steps: result.run.steps.length, usage: result.run.usage,
123
+ });
124
+ }
125
+ finally {
126
+ if (previous)
127
+ previous.busy = false;
128
+ }
129
+ },
130
+ }),
131
+ ];
132
+ const readOnly = new Set(dataTools.map((tool) => tool.name));
133
+ for (const tool of tools) {
134
+ server.registerTool(tool.name, {
135
+ description: tool.description,
136
+ inputSchema: fromJsonSchema(tool.parameters),
137
+ annotations: { readOnlyHint: readOnly.has(tool.name), destructiveHint: false, openWorldHint: !readOnly.has(tool.name) },
138
+ }, async (args, request) => {
139
+ const signal = AbortSignal.any([lifetime.signal, request.mcpReq.signal]);
140
+ const context = { run_id: randomUUID(), cwd, signal, approve: denyAll };
141
+ try {
142
+ signal.throwIfAborted();
143
+ return { content: [{ type: 'text', text: await tool.execute(args, context) }] };
144
+ }
145
+ catch (error) {
146
+ return { isError: true, content: [{ type: 'text', text: `错误:${error instanceof Error ? error.message : String(error)}` }] };
147
+ }
148
+ });
149
+ }
150
+ server.server.onclose = () => {
151
+ lifetime.abort();
152
+ datasets.clear();
153
+ conversations.clear();
154
+ };
155
+ return server;
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugc-lens",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "面向评论、弹幕等用户互动内容的轻量分析 Agent",
5
5
  "keywords": [
6
6
  "ugc",
@@ -32,12 +32,17 @@
32
32
  "types": "./dist/index.d.ts",
33
33
  "default": "./dist/index.js"
34
34
  },
35
- "./package.json": "./package.json"
35
+ "./package.json": "./package.json",
36
+ "./mcp": {
37
+ "types": "./dist/mcp/server.d.ts",
38
+ "default": "./dist/mcp/server.js"
39
+ }
36
40
  },
37
41
  "main": "./dist/index.js",
38
42
  "types": "./dist/index.d.ts",
39
43
  "files": [
40
- "dist"
44
+ "dist",
45
+ "skills"
41
46
  ],
42
47
  "engines": {
43
48
  "node": ">=22.18"
@@ -63,10 +68,12 @@
63
68
  "smoke": "node examples/00_hello.ts && node examples/01_list_models.ts && node examples/03_agent_loop.ts 1 && node examples/04_tools.ts < /dev/null && node examples/06_retry_budget.ts retry && node examples/07_dataset_stats.ts --fresh && node examples/08_ask.ts \"评论里有人提到价格吗?\" && node examples/09_chunks.ts && node examples/09_summarize.ts && node examples/09_analyze.ts > /dev/null"
64
69
  },
65
70
  "devDependencies": {
71
+ "@modelcontextprotocol/client": "2.4.0",
66
72
  "@types/node": "22.20.5",
67
73
  "typescript": "7.0.2"
68
74
  },
69
75
  "dependencies": {
76
+ "@modelcontextprotocol/server": "2.4.0",
70
77
  "zod": "4.6.7"
71
78
  }
72
79
  }
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: ugc-lens
3
+ description: 使用 ugc-lens MCP 工具分析本地 JSON 中的评论、回复和弹幕,生成带原文证据的报告或连续追问;不负责搜集平台数据。
4
+ ---
5
+
6
+ # ugc-lens 数据分析
7
+
8
+ 用户提供的是已经搜集好的 JSON 文件。文件结构由 ugc-lens 识别,不要自行假设字段名,也不要把整个大文件读进上下文。工具名前缀由宿主决定,以下使用服务内名称。
9
+
10
+ ## 选择工具
11
+
12
+ - 需要完整分析报告时调用 `analyze`,用 `file` 指定路径,可用 `directions` 指定用户关注点。工具会自动导入、运行流水线并保存报告;向用户给出返回的报告路径。
13
+ - 宿主自己回答具体问题时,先 `load_dataset`,再用 `get_dataset_stats`、`search_feedback` 和 `get_feedback` 查证。只有一个数据集时可以省略 `dataset_id`;有多个时用 `list_datasets` 选择。
14
+ - 需要 ugc-lens 配置的模型代为查询并回答时调用 `ask`,第一次用 `file` 或 `dataset_id` 和 `question`;同一数据集连续追问时传回 `conversation_id`,不需要重复导入。新问题需要独立上下文时不传这个 ID。
15
+
16
+ ## 查询和证据
17
+
18
+ `search_feedback` 的关键词以空格表示同时匹配,以 `|` 表示任意匹配;支持按点赞、类型、属地等筛选。根据返回的下一页 `offset` 翻页。搜索结果正文会截断,需要全文或回复链时调用 `get_feedback`。
19
+
20
+ 结论用工具结果里真实出现的 `[ID]` 引用,保留原文。区分样本中的支持数量和全平台比例;回复可能未全部搜集,不据此推断没有回复。`ask` 返回 `citations.invalid` 时不要把这些 ID 当作证据。
21
+
22
+ ## 路径和运行
23
+
24
+ 相对路径以 MCP 启动时 `--cwd` 指定的目录为准。宿主与服务工作目录不同时使用绝对路径。数据集和追问历史仅在本次服务进程内保留,重启后需要重新导入;相同文件的结构映射可从磁盘缓存复用。
25
+
26
+ 握手和工具发现不需要 API key;导入时识别结构、`analyze` 和 `ask` 使用 ugc-lens 的模型配置。配置缺失时说明错误并指导用户运行 `ugc-lens init`,不要要求用户把 key 发进聊天或写进可提交的宿主配置。
27
+
28
+ `analyze` 会写报告,模型调用也会产生用量。按用户任务选择工具,不为了一个具体问题重复生成整份报告。工具报错时根据错误修正路径或参数,实际成功后再说明完成。