
Ima Skill
- 1 installs
- 1 repo stars
- Updated April 4, 2026
- guohuiyuan/ima-skill
Manage Tencent IMA notes and knowledge bases - upload files, save web pages, search content, and create or edit notes via the IMA OpenAPI.
About
Unifies note management and knowledge-base operations against the official IMA OpenAPI, triggering on intents like saving, searching, or editing notes and knowledge-base content. A developer uses it when integrating IMA note-taking and knowledge storage into an agent.
- Note create/search/edit plus knowledge-base file and web upload
- Uses user-provided IMA OpenAPI credentials sent only to ima.qq.com
Ima Skill by the numbers
- 1 all-time installs (skills.sh)
- Ranked #2,476 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/guohuiyuan/ima-skill --skill ima-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 1 |
| Last updated | April 4, 2026 |
| Repository | guohuiyuan/ima-skill ↗ |
What it does
Manage Tencent IMA notes and knowledge bases - upload files, save web pages, search content, and create or edit notes via the IMA OpenAPI.
Files
Knowledge Base (知识库)
Prerequisites: see root../SKILL.mdfor setup, credentials, andima_api()helper.
API base path: openapi/wiki/v1
通过 IMA Wiki OpenAPI 管理用户知识库,支持上传文件、添加网页链接、搜索知识库内容、浏览知识库列表和获取知识库详情。
完整的数据结构和接口参数详见 references/api.md。
接口决策表
| 用户意图 | 调用接口 | 关键参数 |
|---|---|---|
| 上传文件到知识库 | check_repeated_names → create_media → COS Upload → add_knowledge | media_type(按扩展名),knowledge_base_id,file_name,file_size |
| 上传文件到知识库的某个文件夹 | 先定位文件夹 → 同上(folder_id 传入目标文件夹 ID) | 见「文件夹操作」章节 |
| 添加网页/微信文章到知识库 | import_urls | urls(1-10 个),knowledge_base_id,可选 folder_id(省略则根目录) |
| 添加笔记到知识库 | add_knowledge | media_type=11,note_info.content_id=<doc_id>,knowledge_base_id |
| 添加 URL(文件型)到知识库 | check_repeated_names → 下载文件 → 走"上传文件"流程 | URL 指向 PDF/Word/PPT 等文件时,按文件方式处理 |
| 检查文件名是否重复 | check_repeated_names | params[].name,params[].media_type,knowledge_base_id,folder_id |
| 获取知识库信息 | get_knowledge_base | ids(1-20 个,不重复) |
| 浏览知识库内容列表 / 浏览文件夹 | get_knowledge_list | knowledge_base_id,cursor,limit(1~50),可选 folder_id |
| 在知识库中搜索(含文件和文件夹) | search_knowledge | query,knowledge_base_id,cursor |
| 按关键词查找知识库(用户知道名字但不知道 ID) | search_knowledge_base | query,cursor,limit(1~50) |
| 查看/了解自己有哪些知识库 | search_knowledge_base(query 传空字符串) | query: "",cursor,limit(1~50) |
| 添加内容但未指定目标知识库 | get_addable_knowledge_base_list → 展示列表让用户选择 | cursor,limit(1~50) |
search_knowledge_base vs get_addable_knowledge_base_list 选择指南
这两个接口容易混淆,选择规则:
| 场景 | 使用接口 | 原因 |
|---|---|---|
| 用户说了知识库名称(如"添加到产品文档库") | search_knowledge_base | 按名称搜索,找到 ID 后继续操作 |
| 用户想浏览/了解某个知识库 | search_knowledge_base → get_knowledge_base | 先搜到 ID,再获取详情 |
| 用户想查看自己有哪些知识库(无具体关键词) | search_knowledge_base(query: "") | 空 query 返回用户的所有知识库列表 |
| 用户要添加内容但没说添加到哪个知识库 | get_addable_knowledge_base_list | 列出有权限添加的知识库,让用户选择 |
| 用户说"添加到知识库"但上下文中无法确定哪个知识库 | get_addable_knowledge_base_list | 同上,不要猜测,让用户选择 |
绝不要在用户已明确指定知识库名称时调用 get_addable_knowledge_base_list,直接用 search_knowledge_base 按名称搜索即可。
文件类型检测
使用 scripts/preflight-check.cjs 脚本自动完成类型检测和大小校验。脚本按以下优先级解析:
1. `--content-type` 已提供且可识别 → content-type 优先,直接使用 2. `--content-type` 不可识别 → 回退到扩展名 3. 未提供 `--content-type` → 使用扩展名 4. 两者都无法识别 → 拒绝处理
# 有扩展名(自动推断)
node .claude/skills/ima-skill/knowledge-base/scripts/preflight-check.cjs --file report.pdf
# 无扩展名或扩展名不可识别(需传入 content-type,如从 HTTP HEAD 获取)
node .claude/skills/ima-skill/knowledge-base/scripts/preflight-check.cjs --file downloaded_file --content-type application/pdf扩展名与类型的对应关系:
| 扩展名 | media_type | content_type |
|---|---|---|
.pdf | 1 | application/pdf |
.doc | 3 | application/msword |
.docx | 3 | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
.ppt | 4 | application/vnd.ms-powerpoint |
.pptx | 4 | application/vnd.openxmlformats-officedocument.presentationml.presentation |
.xls | 5 | application/vnd.ms-excel |
.xlsx | 5 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
.csv | 5 | text/csv |
.md / .markdown | 7 | text/markdown |
.png | 9 | image/png |
.jpg / .jpeg | 9 | image/jpeg |
.webp | 9 | image/webp |
.txt | 13 | text/plain |
.xmind | 14 | application/x-xmind / application/vnd.xmind.workbook / application/zip |
.mp3 | 15 | audio/mpeg |
.m4a | 15 | audio/x-m4a |
.wav | 15 | audio/wav |
.aac | 15 | audio/aac |
未识别的扩展名或无扩展名:直接告知用户该文件类型不被支持,立即终止操作。不要猜测或默认为某个类型,不要询问用户是否仍要上传。
不支持的类型:视频文件(.mp4、.avi、.mov等)、Bilibili(bilibili.com/video/)和 YouTube(youtube.com/watch)链接、本地 HTML 文件(file://)无法通过 skill 添加到知识库。直接告知用户「该文件类型不支持,仅支持在 ima 桌面端内添加进知识库」,不要提供上传选项或询问是否继续。
URL 类型检测
添加 URL 到知识库时,需要根据 URL 模式和 Content-Type 判断类型。检测按以下优先级进行:
1. Content-Type 为 `text/html` 时,按 URL 模式区分:
| URL 模式 | media_type | 类型 | 处理方式 |
|---|---|---|---|
匹配 mp.weixin.qq.com/s/ 或 mp.weixin.qq.com/s? | 6 | 微信公众号文章 | 使用 import_urls |
以 https://www.bilibili.com/video/ 开头 | ❌ 16 | 视频网页 | 不支持,告知用户「仅支持在 ima 桌面端内添加进知识库」 |
以 https://www.youtube.com/watch 开头 | ❌ 16 | 视频网页 | 不支持,告知用户「仅支持在 ima 桌面端内添加进知识库」 |
以 file:// 开头 | ❌ | 本地 HTML | 不支持,告知用户「仅支持在 ima 桌面端内添加进知识库」 |
其他 text/html 页面 | 2 | 普通网页 | 使用 import_urls |
2. Content-Type 为文件类型时: 按文件类型检测表处理(PDF、Word、Excel 等)。
3. 其他: 告知用户该类型不被支持。
添加前置检查(Pre-flight Check)
在执行任何添加知识到知识库的操作前(add_knowledge、import_urls、上传文件流程),必须按以下顺序逐项检查,任一项不通过则立即终止并告知用户,不要询问是否仍要尝试上传:
1. 类型支持检查
| 检查项 | 条件 | 不通过时的处理 |
|---|---|---|
| 文件扩展名是否支持 | 扩展名不在「文件类型检测」表中 | 告知用户该文件类型不被支持 |
| 视频文件 | .mp4、.avi、.mov 等视频扩展名 | 告知用户「仅支持在 ima 桌面端内添加进知识库」 |
| 视频网页 URL | https://www.bilibili.com/video/ 或 https://www.youtube.com/watch 开头 | 告知用户「仅支持在 ima 桌面端内添加进知识库」 |
| 本地 HTML 文件 | file:// 协议 | 告知用户「仅支持在 ima 桌面端内添加进知识库」 |
2. 文件大小检查
上传前必须校验文件大小,超限文件应在上传前拦截,不要发起请求:
| 文件类型 | media_type | 最大大小 |
|---|---|---|
| Excel、TXT、Xmind、Markdown | 5/13/14/7 | 10 MB |
| 图片 | 9 | 30 MB |
| PDF、Word、PPT、音频及其他 | 1/3/4/15 等 | 200 MB |
3. 音频时长检查
音频文件(media_type=15)额外限制:最长 2 小时。超过时告知用户。
4. 文件名重复检查
仅适用于文件类型(media_type 1/3/4/5/7/9/13/14/15),不适用于网页(2/6)、笔记(11)等:
- 调用
check_repeated_names检查 is_repeated=true:询问用户是否保留两者(追加时间戳)或取消- 不支持"替换"操作
检查顺序很重要:先做类型和大小检查(本地即可判断),通过后再调用远程接口检查重名。避免对不支持或超限的文件发起不必要的网络请求。
常用工作流
上传文件到知识库
完成「添加前置检查」后,执行以下步骤:创建媒体 → 上传 COS → 添加知识。
前置检查(类型检测、大小校验、重名检查)见「添加前置检查」章节。
# 1. 前置检查 — 类型、大小一步完成
# 有扩展名时:自动从扩展名推断 media_type 和 content_type
# 无扩展名时:需通过 --content-type 传入(如从 HTTP HEAD 获取)
PREFLIGHT=$(node .claude/skills/ima-skill/knowledge-base/scripts/preflight-check.cjs \
--file "/path/to/report.pdf")
echo "$PREFLIGHT"
# pass=false 时直接终止,将 reason 展示给用户
# 2. 从 preflight 结果提取字段(用于后续 API 调用)
FILE_NAME=$(echo "$PREFLIGHT" | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));process.stdout.write(d.file_name)")
FILE_EXT=$(echo "$PREFLIGHT" | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));process.stdout.write(d.file_ext)")
FILE_SIZE=$(echo "$PREFLIGHT" | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));process.stdout.write(String(d.file_size))")
MEDIA_TYPE=$(echo "$PREFLIGHT" | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));process.stdout.write(String(d.media_type))")
CONTENT_TYPE=$(echo "$PREFLIGHT" | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));process.stdout.write(d.content_type)")
# 3. 重名检查(仅文件类型,见「添加前置检查」第 4 步)
# 4. create_media — 获取 media_id 和 COS 上传凭证
ima_api "openapi/wiki/v1/create_media" "{
\"file_name\": \"$FILE_NAME\",
\"file_size\": $FILE_SIZE,
\"content_type\": \"$CONTENT_TYPE\",
\"knowledge_base_id\": \"<kb_id>\",
\"file_ext\": \"$FILE_EXT\"
}"
# 从返回值提取 media_id 和 cos_credential 各字段
# 5. 上传文件到 COS
node .claude/skills/ima-skill/knowledge-base/scripts/cos-upload.cjs \
--file "/path/to/report.pdf" \
--secret-id "<cos_credential.secret_id>" \
--secret-key "<cos_credential.secret_key>" \
--token "<cos_credential.token>" \
--bucket "<cos_credential.bucket_name>" \
--region "<cos_credential.region>" \
--cos-key "<cos_credential.cos_key>" \
--content-type "$CONTENT_TYPE" \
--start-time "<cos_credential.start_time>" \
--expired-time "<cos_credential.expired_time>"
# 6. add_knowledge — 将已上传的文件关联到知识库
ima_api "openapi/wiki/v1/add_knowledge" "{
\"media_type\": $MEDIA_TYPE,
\"media_id\": \"<media_id>\",
\"title\": \"$FILE_NAME\",
\"knowledge_base_id\": \"<kb_id>\",
\"file_info\": {
\"cos_key\": \"<cos_credential.cos_key>\",
\"file_size\": $FILE_SIZE,
\"file_name\": \"$FILE_NAME\"
}
}"批量上传时的重复处理
当上传多个文件时,可一次性检查所有文件名(最多 2000 个):
# 批量检查
ima_api "openapi/wiki/v1/check_repeated_names" '{
"params": [
{"name": "report.pdf", "media_type": 1},
{"name": "slides.pptx", "media_type": 4},
{"name": "data.xlsx", "media_type": 5}
],
"knowledge_base_id": "<kb_id>",
"folder_id": "<folder_id>"
}'
# 注意:如果是根目录,省略 folder_id 字段
# 遍历 results,对 is_repeated=true 的文件询问用户:
# - "以下文件在知识库中已存在同名文件:report.pdf、data.xlsx。是否保留两者?(不支持替换)"
# - 用户选择"保留两者"的文件:追加时间戳后继续上传
# - 用户选择"取消"的文件:从上传列表中移除时间戳命名规则:在文件名(不含扩展名)末尾追加 _YYYYMMDDHHmmss,例如 report_20260317153000.pdf。
添加网页/微信文章到知识库
使用 import_urls 批量导入网页和微信公众号文章(1-10 个 URL),服务端自动识别类型:
# 添加到根目录(不传 folder_id)
ima_api "openapi/wiki/v1/import_urls" '{
"knowledge_base_id": "<kb_id>",
"urls": [
"https://example.com/article",
"https://mp.weixin.qq.com/s/xxxxx"
]
}'
# 添加到指定文件夹(传 folder_id,以 folder_ 开头)
ima_api "openapi/wiki/v1/import_urls" '{
"knowledge_base_id": "<kb_id>",
"folder_id": "<folder_id>",
"urls": [
"https://example.com/article"
]
}'
# 返回 results 映射:{ "<url>": { url, ret_code, media_id } }
# ret_code=0 表示成功,非 0 查看 errmsg添加笔记到知识库
将已有笔记(通过 doc_id 引用)直接关联到知识库,无需下载内容:
ima_api "openapi/wiki/v1/add_knowledge" '{
"media_type": 11,
"note_info": { "content_id": "<doc_id>" },
"title": "笔记标题",
"knowledge_base_id": "<kb_id>"
}'添加 URL 到知识库(自动检测文件型 URL)
当用户提供 URL 时,需先判断该 URL 指向的是网页还是可下载文件(PDF、Word、PPT 等)。
判断规则(按优先级):
1. URL 路径包含文件扩展名:如 https://arxiv.org/pdf/2603.12268 以 /pdf/ 开头,或 https://example.com/report.pdf 以 .pdf 结尾 → 文件型 2. 发送 HEAD 请求检查 Content-Type:curl -sI -L <url> 查看响应头
application/pdf→ PDF 文件application/msword或application/vnd.openxmlformats-*→ Word/PPT/Excel 文件text/html→ 网页
3. 已知文件型 URL 模式:
arxiv.org/pdf/*→ PDF*.pdf、*.docx、*.pptx、*.xlsx结尾 → 对应文件类型- GitHub raw 文件链接 → 按扩展名判断
文件型 URL 处理流程:
# 1. 探测 URL 类型
CONTENT_TYPE=$(curl -sI -L "https://arxiv.org/pdf/2603.12268" | grep -i "^content-type:" | tail -1 | awk '{print $2}' | tr -d '\r')
# 结果如 application/pdf → 文件型
# 2. 下载文件到临时目录
TEMP_DIR=$(mktemp -d)
# 根据 Content-Type 或 URL 推断文件名和扩展名
curl -sL -o "$TEMP_DIR/paper.pdf" "https://arxiv.org/pdf/2603.12268"
# 3. 前置检查(传入 content-type 作为备用,文件名有扩展名时会优先用扩展名)
PREFLIGHT=$(node .claude/skills/ima-skill/knowledge-base/scripts/preflight-check.cjs \
--file "$TEMP_DIR/paper.pdf" --content-type "$CONTENT_TYPE")
echo "$PREFLIGHT"
# pass=false 时直接终止
# 4. 按"上传文件到知识库"流程处理:create_media → COS Upload → add_knowledge
# (参见上方"上传文件到知识库"工作流)
# 5. 清理临时文件
rm -rf "$TEMP_DIR"文件名推断:
- 优先从
Content-Disposition响应头提取文件名 - 其次从 URL 路径中提取(如
/pdf/2603.12268→2603.12268.pdf) - 最后使用 URL 的最后一段路径 + 根据 Content-Type 补充扩展名
文件夹操作
知识库内容以文件夹结构组织。文件夹本身也是一种知识条目,在 get_knowledge_list 和 search_knowledge 的返回结果中会同时包含文件和文件夹。
核心概念
folder_id:文件夹的唯一标识,始终以 `folder_` 前缀开头(如folder_abc123),在add_knowledge、import_urls、get_knowledge_list、check_repeated_names等接口中用于指定目标文件夹- 操作根目录时,不要传 `folder_id` 参数(直接省略该字段),不要将
knowledge_base_id作为folder_id传入 get_knowledge_list返回的current_path(FolderInfo[])表示当前浏览位置的完整路径(面包屑)
定位文件夹(用户提到文件夹名时)
当用户说「添加到 XX 文件夹」但只给了文件夹名称时,需要先找到 folder_id:
# 方法 1:搜索知识库内容(推荐,可直接按名称搜索文件夹)
ima_api "openapi/wiki/v1/search_knowledge" '{
"query": "文件夹名称",
"knowledge_base_id": "<kb_id>",
"cursor": ""
}'
# 从返回的 info_list 中找到匹配的文件夹条目,取其 media_id 作为 folder_id
# 方法 2:浏览根目录列表逐级查找
ima_api "openapi/wiki/v1/get_knowledge_list" '{
"knowledge_base_id": "<kb_id>",
"cursor": "",
"limit": 50
}'
# 从返回的 knowledge_list 中找到目标文件夹,取其 media_id 作为 folder_id
# 如果文件夹在子目录中,需要用返回的 folder_id 逐级深入添加内容到指定文件夹
所有写入接口(add_knowledge、import_urls、check_repeated_names)都支持 folder_id 参数:
# 上传文件到指定文件夹
ima_api "openapi/wiki/v1/add_knowledge" '{
"media_type": 1,
"media_id": "<media_id>",
"title": "report.pdf",
"knowledge_base_id": "<kb_id>",
"folder_id": "<folder_id>",
"file_info": { "cos_key": "...", "file_size": 12345, "file_name": "report.pdf" }
}'
# 导入网页到指定文件夹
ima_api "openapi/wiki/v1/import_urls" '{
"knowledge_base_id": "<kb_id>",
"folder_id": "<folder_id>",
"urls": ["https://example.com/article"]
}'
# 添加到根目录时,直接省略 folder_id
ima_api "openapi/wiki/v1/add_knowledge" '{
"media_type": 1,
"media_id": "<media_id>",
"title": "report.pdf",
"knowledge_base_id": "<kb_id>",
"file_info": { "cos_key": "...", "file_size": 12345, "file_name": "report.pdf" }
}'获取知识库信息
ima_api "openapi/wiki/v1/get_knowledge_base" '{"ids": ["<kb_id>"]}'
# 返回 infos 映射:{ "<kb_id>": { id, name, cover_url, description, recommended_questions } }浏览知识库内容
# 浏览根目录
ima_api "openapi/wiki/v1/get_knowledge_list" '{"knowledge_base_id": "<kb_id>", "cursor": "", "limit": 20}'
# 浏览指定文件夹
ima_api "openapi/wiki/v1/get_knowledge_list" '{"knowledge_base_id": "<kb_id>", "folder_id": "<folder_id>", "cursor": "", "limit": 20}'
# 翻页:用 next_cursor,is_end=true 时停止在知识库中搜索
ima_api "openapi/wiki/v1/search_knowledge" '{"query": "搜索关键词", "knowledge_base_id": "<kb_id>", "cursor": ""}'搜索知识库列表
# 按关键词搜索
ima_api "openapi/wiki/v1/search_knowledge_base" '{"query": "搜索关键词", "cursor": "", "limit": 20}'
# 查看所有知识库(空 query)
ima_api "openapi/wiki/v1/search_knowledge_base" '{"query": "", "cursor": "", "limit": 20}'获取可添加的知识库列表
仅当用户要添加内容但未指定目标知识库时使用。如果用户已给出知识库名称,应使用 search_knowledge_base 按名称搜索,而非此接口。
# 首次请求
ima_api "openapi/wiki/v1/get_addable_knowledge_base_list" '{"cursor": "", "limit": 20}'
# 翻页:用 next_cursor,is_end=true 时停止核心响应字段
知识条目(KnowledgeInfo)关键字段:media_id(媒体ID)、title、parent_folder_id。
搜索到的知识条目(SearchedKnowledgeInfo)关键字段:media_id、title、parent_folder_id、highlight_content(高亮内容,内容匹配时返回)。
知识库信息(KnowledgeBaseInfo)关键字段:id、name、cover_url、description、recommended_questions。
文件夹条目(FolderInfo)关键字段:folder_id、name、file_number、folder_number、parent_folder_id、is_top。
完整字段定义见 references/api.md。
分页
所有列表和搜索接口使用游标分页:
1. 首次请求:cursor: "" 2. 检查返回的 is_end:false 表示还有更多数据 3. 将返回的 next_cursor 作为下次请求的 cursor 4. is_end = true 时停止翻页
响应处理
所有 API 返回统一结构 { "retcode": 0, "errmsg": "...", "data": { ... } }:
retcode=0:成功,从data提取业务字段retcode≠0:失败,直接将 `errmsg` 展示给用户即可,不需要自行翻译错误码
用户体验
- 隐藏内部 ID:面向用户的展示中永远不要暴露 `knowledge_base_id`、`media_id`、`folder_id` 等内部 ID。始终使用知识库名称、文件标题、文件夹名称等用户可读信息。ID 仅用于后续 API 调用,不展示给用户。
- ✅
"已添加到知识库「产品文档库」✓" - ❌
"已添加到知识库 abc123def456 ✓" - 需要引用知识库时,先通过
get_knowledge_base获取名称,再展示 - 精简进度:不要逐步暴露内部操作(如"正在创建媒体…正在上传 COS…")。只报告用户关心的信息:
- 上传文件:
"正在上传 report.pdf…"→"已添加到知识库「产品文档库」✓" - 添加网页:
"正在添加…"→"已添加到「产品文档库」✓" - 失败时展示
errmsg即可 - 批量操作:汇总结果,如
"3 个文件已添加到「产品文档库」,1 个失败(data.xlsx: 文件大小超限)" - 格式化展示:读取类操作的结果应以结构化格式展示给用户,而非原始 JSON:
知识库列表(search_knowledge_base / get_addable_knowledge_base_list):
搜索知识库后,用返回的 ID 列表调用 get_knowledge_base 获取描述信息,一并展示。 📚 搜索结果(共 3 个知识库):
1. **产品文档库** — 存放产品相关的所有文档资料
2. **技术方案库** — 各项目技术方案汇总
3. **竞品分析库**知识库内容列表(get_knowledge_list):
📂 知识库「产品文档库」内容:
📁 设计文档/ (3 个文件, 1 个子文件夹)
📁 会议纪要/ (12 个文件)
📄 产品需求文档.pdf
📄 技术方案.docx
📄 数据分析.xlsx
--- 第 1 页,还有更多内容 ---搜索结果(search_knowledge):
search_knowledge返回的条目包含media_id、title、parent_folder_id、highlight_content(内容匹配时返回高亮片段),
🔍 在知识库「产品文档库」中搜索「排期」的结果:
1. 📄 Q1排期表.xlsx (文件夹: 项目管理/)
> ...包含**排期**计划的详细信息...
2. 📄 开发排期讨论.pdf (文件夹: 会议纪要/)
3. 📁 排期模板/ (文件夹: 根目录)知识库详情(get_knowledge_base):
📚 产品文档库
📝 描述:存放产品相关的所有文档资料
💡 推荐问题:
- 最新的产品需求是什么?
- 技术方案有哪些?注意事项
get_knowledge_base接受 1-20 个 ID;单个 ID 也需包装为数组get_knowledge_list的limit范围为 1~50- 文件夹是知识条目的一种:
get_knowledge_list和search_knowledge的返回结果中同时包含文件和文件夹,需通过字段区分(文件夹有folder_id/name/file_number/folder_number,文件有media_id/title) - 用户提到文件夹时:如果用户只给了文件夹名称(而非 ID),必须先通过
search_knowledge或get_knowledge_list找到对应的folder_id,再执行后续操作 folder_id在add_knowledge、import_urls、get_knowledge_list、check_repeated_names中均为可选字段,操作根目录时直接省略 `folder_id`,不要传该参数。folder_id的值始终以folder_前缀开头(如folder_abc123),不要将 `knowledge_base_id` 作为 `folder_id` 传入- 文件上传时 `title` 必须等于 `file_name`:调用
add_knowledge添加文件时,title字段必须使用文件的原始完整文件名(含扩展名),不要自行拟定标题。file_name和title传同一个值。禁止缩短、翻译、重命名或省略任何部分。例如文件名为音频.mp3,则file_name和title都必须传音频.mp3 - 文件扩展名必须正确提取,用于
media_type检测和file_ext字段(无点号,如pdf) - COS 上传脚本失败(非零退出码)时,不要继续调用
add_knowledge - COS 上传时
--content-type应传入文件的实际 MIME 类型(如application/pdf),而非通用的application/octet-stream - 当用户提供 URL 添加到知识库时,必须先检测 URL 是否指向文件(通过 URL 路径扩展名 + HEAD 请求 Content-Type),文件型 URL 需下载后走上传流程;网页/微信文章型 URL 使用
import_urls
IMA知识库 API
⚠️ 必读约束
🌐 服务信息
- Base URL :
https://ima.qq.com - Base Path:
/openapi/wiki/v1 - 协议:HTTP POST,JSON body
- 完整示例:
POST https://ima.qq.com/openapi/wiki/v1/get_knowledge_base
🔒 认证
所有请求必须携带 Header:
| Header | 说明 |
|---|---|
ima-openapi-clientid | Client ID |
ima-openapi-apikey | API Key |
Content-Type | application/json |
---
快速决策
| 用户意图 | 接口 |
|---|---|
| 「上传文件到知识库」 | check_repeated_names → create_media → COS Upload → add_knowledge |
| 「上传文件到指定文件夹」 | 先定位文件夹 → 同上(传入 folder_id) |
| 「添加网页/微信文章到知识库」 | import_urls |
| 「获取知识库信息」 | get_knowledge_base |
| 「浏览知识库内容 / 浏览文件夹」 | get_knowledge_list(可传 folder_id 进入子文件夹) |
| 「在知识库中搜索」 | search_knowledge |
| 「搜索知识库列表」 | search_knowledge_base |
| 「获取可添加的知识库列表」 | get_addable_knowledge_base_list |
| 「检查文件名是否重复」 | check_repeated_names |
---
数据结构
KnowledgeBaseInfo(知识库信息)
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 知识库唯一 ID |
name | string | 知识库名称 |
cover_url | string | 封面图 URL |
description | string | 描述 |
recommended_questions | string[] | 推荐问题列表 |
KnowledgeInfo(知识条目)
| 字段 | 类型 | 说明 |
|---|---|---|
media_id | string | 媒体 ID |
title | string | 标题 |
parent_folder_id | string | 所属文件夹 ID |
FolderInfo(文件夹条目)
| 字段 | 类型 | 说明 |
|---|---|---|
folder_id | string | 文件夹 ID |
name | string | 文件夹名称 |
file_number | int64 | 文件数 |
folder_number | int64 | 子文件夹数 |
parent_folder_id | string | 父文件夹 ID |
is_top | bool | 是否置顶 |
AddableKnowledgeBaseInfo(可添加的知识库信息)
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 知识库 ID |
name | string | 知识库名称 |
SearchedKnowledgeBaseInfo(搜索到的知识库信息)
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 知识库 ID |
name | string | 知识库名称 |
cover_url | string | 封面图 URL |
SearchedKnowledgeInfo(搜索到的知识条目)
| 字段 | 类型 | 说明 |
|---|---|---|
media_id | string | 媒体 ID |
title | string | 标题 |
parent_folder_id | string | 所属文件夹 ID |
highlight_content | string | 高亮内容(内容匹配时返回) |
ContentInfo(内容信息)
| 字段 | 类型 | 说明 |
|---|---|---|
content_id | string | 内容 ID(网页时为 URL) |
ImportURLData(URL 导入结果)
| 字段 | 类型 | 说明 |
|---|---|---|
url | string | 导入的 URL |
ret_code | int32 | 0=成功,非 0=失败 |
media_id | string | 导入成功后返回的媒体 ID |
FileInfo(文件信息)
add_knowledge 文件上传时使用:
| 字段 | 类型 | 说明 |
|---|---|---|
cos_key | string | COS 对象 Key |
file_size | uint64 | 文件大小(字节) |
last_modify_time | int64 | 最后修改时间(秒级时间戳) |
password | string | 文件密码(如有) |
file_name | string | 文件名称 |
Credential(COS 上传凭证)
create_media 返回,用于上传文件到腾讯云 COS:
| 字段 | 类型 | 说明 |
|---|---|---|
token | string | 临时 TOKEN |
secret_id | string | 临时 Secret ID |
secret_key | string | 临时 Secret Key |
start_time | int64 | 凭证开始时间(秒级时间戳) |
expired_time | int64 | 凭证过期时间(秒级时间戳) |
appid | string | COS AppID |
bucket_name | string | COS 桶名称 |
region | string | COS 桶所在区域 |
custom_domain | string | 自定义域名 |
cos_key | string | COS 对象 Key |
MediaType(媒体类型枚举)
| 值 | 名称 | content_type / 说明 |
|---|---|---|
| 1 | application/pdf | |
| 2 | 网页 | N/A(直接 AddKnowledge,web_info.content_id=<url>) |
| 3 | Word | application/msword / application/vnd.openxmlformats-officedocument.wordprocessingml.document |
| 4 | PPT | application/vnd.ms-powerpoint / application/vnd.openxmlformats-officedocument.presentationml.presentation |
| 5 | Excel | application/vnd.ms-excel / application/vnd.openxmlformats-officedocument.spreadsheetml.sheet / text/csv |
| 6 | 微信公众号文章 | N/A(直接 AddKnowledge,web_info.content_id=<url>,URL 匹配 mp.weixin.qq.com/s) |
| 7 | MarkDown | text/markdown / text/x-markdown / application/md / application/markdown |
| 9 | 图片 | image/png, image/jpeg, image/webp |
| 11 | 笔记 | N/A(直接 AddKnowledge,note_info.content_id=<doc_id>) |
| 12 | AI会话 | N/A(直接 AddKnowledge,session_info.content_id=<session_id>) |
| 13 | TXT | text/plain |
| 14 | Xmind | application/x-xmind / application/vnd.xmind.workbook / application/zip |
| 15 | 录音 | audio/mpeg(mp3), audio/x-m4a(m4a), audio/wav(wav), audio/aac(aac) |
| 16 | 视频解析 | 不支持通过 skill 添加。Bilibili/YouTube/本地HTML等仅支持在 ima 桌面端内添加进知识库 |
---
接口详情
1. 创建媒体
POST /openapi/wiki/v1/create_media
触发场景:上传文件到知识库的第一步,获取 COS 上传凭证。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file_name | string | 是 | 文件名称(最长 1024 字符) |
file_size | uint64 | 是 | 文件大小(字节) |
content_type | string | 是 | MIME 类型 |
knowledge_base_id | string | 是 | 知识库 ID |
file_ext | string | 是 | 文件后缀名(无点号,如 pdf) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
media_id | string | 媒体 ID |
cos_credential | Credential | COS 上传凭证 |
---
2. 添加知识
POST /openapi/wiki/v1/add_knowledge
触发场景:上传文件到知识库的最后一步,或直接添加网页 URL。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
media_type | int32 | 是 | 媒体类型 |
media_id | string | 否 | 文件上传时必填,CreateMedia 返回的 ID |
title | string | 是 | 标题 |
knowledge_base_id | string | 是 | 知识库 ID |
folder_id | string | 否 | 文件夹 ID(省略则添加到根目录) |
note_info | ContentInfo | 否 | 笔记内容信息 |
web_info | ContentInfo | 否 | 网页内容信息(media_type=2 时必填) |
web_info.content_id | string | 条件必填 | 网页 URL(media_type=2 时必填) |
session_info | ContentInfo | 否 | 会话内容信息 |
file_info | FileInfo | 否 | 文件信息(文件上传时必填,见 FileInfo) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
media_id | string | 媒体 ID |
---
3. 获取知识库信息
POST /openapi/wiki/v1/get_knowledge_base
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
ids | string[] | 是 | 知识库 ID 列表(1-20 个,不重复) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
infos | map\<string, KnowledgeBaseInfo\> | 知识库信息映射 |
---
4. 浏览知识库内容
POST /openapi/wiki/v1/get_knowledge_list
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
cursor | string | 是 | 游标,首次传空字符串 |
limit | uint64 | 是 | 数量限制(1-50) |
knowledge_base_id | string | 是 | 知识库 ID |
folder_id | string | 否 | 文件夹 ID(省略则列出根目录) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
knowledge_list | KnowledgeInfo[] | 知识条目列表 |
is_end | bool | 是否到达列表末尾 |
next_cursor | string | 下页游标 |
current_path | FolderInfo[] | 当前路径 |
---
5. 搜索知识库内容
POST /openapi/wiki/v1/search_knowledge
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
query | string | 是 | 搜索关键词 |
cursor | string | 是 | 游标,首次传空字符串 |
knowledge_base_id | string | 是 | 知识库 ID |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
info_list | SearchedKnowledgeInfo[] | 搜索结果(media_id, title, parent_folder_id, highlight_content) |
is_end | bool | 是否到达列表末尾 |
next_cursor | string | 下页游标 |
---
6. 搜索知识库列表
POST /openapi/wiki/v1/search_knowledge_base
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
query | string | 是 | 搜索关键词 |
cursor | string | 是 | 游标,首次传空字符串 |
limit | uint64 | 是 | 数量限制(1-50) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
info_list | SearchedKnowledgeBaseInfo[] | 搜索结果(id, name, cover_url) |
is_end | bool | 是否到达列表末尾 |
next_cursor | string | 下页游标 |
---
7. 获取可添加的知识库列表
POST /openapi/wiki/v1/get_addable_knowledge_base_list
触发场景:用户想上传文件或添加内容到知识库,但不确定可以添加到哪些知识库时,列出当前用户有权限添加内容的知识库。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
cursor | string | 是 | 游标,首次传空字符串 |
limit | uint64 | 是 | 数量限制(1-50) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
addable_knowledge_base_list | AddableKnowledgeBaseInfo[] | 可添加内容的知识库列表 |
next_cursor | string | 下页游标 |
is_end | bool | 是否到达列表末尾 |
---
8. 检查文件名重复
POST /openapi/wiki/v1/check_repeated_names
触发场景:上传文件到知识库前,检查目标知识库(及文件夹)中是否已存在同名文件。仅用于文件类型(media_type 1/3/4/5/7/9/13/14),不用于网页(2/6)、笔记(11)等。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
params | CheckRepeatedNamesParam[] | 是 | 待检查的文件列表(1-2000 个) |
knowledge_base_id | string | 是 | 知识库 ID |
folder_id | string | 否 | 文件夹 ID(省略则检查根目录) |
CheckRepeatedNamesParam:
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 文件名称 |
media_type | int32 | 媒体类型(见 MediaType 枚举) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
results | CheckRepeatedNamesResult[] | 检查结果 |
CheckRepeatedNamesResult:
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 文件名称 |
is_repeated | bool | true 表示同名文件已存在 |
---
9. 导入 URL
POST /openapi/wiki/v1/import_urls
触发场景:添加网页或微信公众号文章到知识库。替代 add_knowledge 的 media_type=2/6 用法,支持批量导入,服务端自动识别 URL 类型。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
knowledge_base_id | string | 是 | 知识库 ID |
folder_id | string | 是 | 文件夹 ID |
urls | string[] | 是 | URL 列表(1-10 个,每个非空字符串) |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
results | map\<string, ImportURLData\> | URL→结果映射(含 ret_code、media_id) |
---
文件夹说明
知识库内容以文件夹层级结构组织。文件夹是一种特殊的知识条目:
get_knowledge_list返回结果中同时包含 文件(KnowledgeInfo)和 文件夹(FolderInfo),通过current_path字段可获取当前路径的面包屑信息search_knowledge搜索结果中也会包含匹配的文件夹- 所有支持
folder_id参数的接口(add_knowledge、import_urls、get_knowledge_list、check_repeated_names),省略folder_id则操作根目录。根目录的 folder_id 等于 knowledge_base_id,当接口要求folder_id必填时(如import_urls),传knowledge_base_id的值即可表示根目录 - 定位文件夹:当用户只提供文件夹名称时,使用
search_knowledge按名称搜索,或用get_knowledge_list逐级浏览,从返回结果中找到目标文件夹的 ID
---
文件大小限制
上传前必须校验文件大小,超限文件应在上传前拦截:
| 文件类型 | media_type | 最大大小 |
|---|---|---|
| Excel、TXT、Xmind、Markdown | 5/13/14/7 | 10 MB |
| 图片 | 9 | 30 MB |
| PDF、Word、PPT、音频及其他 | 1/3/4/15 等 | 200 MB |
网页(2/6)、笔记(11)等非文件类型无大小限制。音频文件额外限制:最长 2 小时。
---
响应格式
所有 API 返回统一结构:
{
"retcode": 0,
"errmsg": "成功",
"data": { ... }
}retcode=0:成功,从data提取业务字段retcode≠0:失败,直接将 `errmsg` 展示给用户,无需自行翻译错误码
---
游标翻页使用规范
1. 首次请求:cursor 传空字符串 "" 2. 检查返回的 is_end:false 表示还有更多数据 3. 将返回的 next_cursor 作为下次请求的 cursor 4. is_end = true 时停止翻页
---
错误码
| 错误码 | 说明 | 建议处理 |
|---|---|---|
| 0 | 成功 | — |
| 110001 | 参数非法 | 检查请求参数(详见 errmsg) |
| 110002 | 配置非法 | 检查服务配置 |
| 110010 | 下游网络错误 | 可重试 |
| 110011 | 下游逻辑错误 | 不可重试,详见 errmsg |
| 110012 | 接口无效 | 检查接口路径 |
| 110013 | 客户端取消 | 检查请求是否超时 |
| 110020 | 安全打击 | 检查内容是否违规 |
| 110021 | 请求频控 | 降低请求频率后重试 |
| 110030 | 无权限 | 确认操作权限 |
#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const https = require('node:https');
// --- Argument parsing ---
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i += 2) {
const key = argv[i].replace(/^--/, '');
const val = argv[i + 1];
if (!val || val.startsWith('--')) {
console.error(`Missing value for --${key}`);
process.exit(1);
}
args[key] = val;
}
return args;
}
const REQUIRED = ['file', 'secret-id', 'secret-key', 'token', 'bucket', 'region', 'cos-key'];
// --- Crypto helpers ---
function hmacSha1(key, data) {
return crypto.createHmac('sha1', key).update(data).digest('hex');
}
function sha1(data) {
return crypto.createHash('sha1').update(data).digest('hex');
}
// --- COS Authorization header (PUT Object) ---
// Reference: https://cloud.tencent.com/document/product/436/7778
function buildAuthorization({ secretId, secretKey, method, pathname, headers, startTime, expiredTime }) {
const keyTime = `${startTime};${expiredTime}`;
// 1. SignKey = HMAC-SHA1(SecretKey, KeyTime)
const signKey = hmacSha1(secretKey, keyTime);
// 2. HttpString = method\npathname\nparams\nheaders\n
// For PUT, no query params; headers we sign: host, content-length
const headerKeys = Object.keys(headers).sort();
const httpHeaders = headerKeys.map((k) => `${k.toLowerCase()}=${encodeURIComponent(headers[k])}`).join('&');
const httpString = `${method.toLowerCase()}\n${pathname}\n\n${httpHeaders}\n`;
// 3. StringToSign = sha1\nKeyTime\nSHA1(HttpString)\n
const stringToSign = `sha1\n${keyTime}\n${sha1(httpString)}\n`;
// 4. Signature = HMAC-SHA1(SignKey, StringToSign)
const signature = hmacSha1(signKey, stringToSign);
// 5. Build Authorization
const headerList = headerKeys.map((k) => k.toLowerCase()).join(';');
return [
`q-sign-algorithm=sha1`,
`q-ak=${secretId}`,
`q-sign-time=${keyTime}`,
`q-key-time=${keyTime}`,
`q-header-list=${headerList}`,
`q-url-param-list=`,
`q-signature=${signature}`,
].join('&');
}
// --- Upload via PUT Object ---
function upload(args) {
const secretId = args['secret-id'];
const secretKey = args['secret-key'];
const { token } = args;
const { bucket } = args;
const { region } = args;
const cosKey = args['cos-key'];
const filePath = args.file;
const startTime = args['start-time'] || String(Math.floor(Date.now() / 1000));
const expiredTime = args['expired-time'] || String(Math.floor(Date.now() / 1000) + 3600);
const fileContent = fs.readFileSync(filePath);
const hostname = `${bucket}.cos.${region}.myqcloud.com`;
const pathname = `/${cosKey}`;
// Headers to sign
const signHeaders = {
'content-length': String(fileContent.length),
host: hostname,
};
const authorization = buildAuthorization({
secretId,
secretKey,
method: 'PUT',
pathname,
headers: signHeaders,
startTime,
expiredTime,
});
// Use the actual file content type if provided, otherwise fall back to octet-stream
const contentType = args['content-type'] || 'application/octet-stream';
const options = {
hostname,
port: 443,
path: pathname,
method: 'PUT',
headers: {
'Content-Type': contentType,
'Content-Length': fileContent.length,
Authorization: authorization,
'x-cos-security-token': token,
},
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log(`Upload successful (HTTP ${res.statusCode})`);
process.exit(0);
} else {
console.error(`COS upload failed (HTTP ${res.statusCode}): ${body}`);
process.exit(1);
}
});
});
req.on('error', (err) => {
console.error(`COS upload error: ${err.message}`);
process.exit(1);
});
req.write(fileContent);
req.end();
}
// --- Main ---
function main() {
const args = parseArgs(process.argv);
const missing = REQUIRED.filter((k) => !args[k]);
if (missing.length) {
console.error(`Missing required arguments: ${missing.map((k) => `--${k}`).join(', ')}`);
console.error(
`Usage: node cos-upload.cjs --file <path> --secret-id <sid> --secret-key <skey> --token <token> --bucket <bucket> --region <region> --cos-key <key> [--content-type <mime>] [--start-time <ts>] [--expired-time <ts>]`,
);
process.exit(1);
}
const filePath = args.file;
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
process.exit(1);
}
upload(args);
}
main();
#!/usr/bin/env node
'use strict';
/**
* Preflight check for uploading a file to IMA Knowledge Base.
*
* Validates file type, size, and extracts all metadata needed for
* create_media and add_knowledge API calls.
*
* Resolution priority:
* 1. If --content-type is provided and recognized → use it (content-type rules over extension)
* 2. If --content-type is unrecognized, fall back to extension
* 3. If no --content-type, use extension
* 4. If neither can resolve → fail
*
* Usage:
* node preflight-check.cjs --file /path/to/report.pdf
* node preflight-check.cjs --file /path/to/downloaded_file --content-type application/pdf
*
* Output (JSON, always to stdout):
*
* Pass:
* {
* "pass": true,
* "file_path": "/absolute/path/to/report.pdf",
* "file_name": "report.pdf",
* "file_ext": "pdf",
* "file_size": 123456,
* "media_type": 1,
* "content_type": "application/pdf"
* }
*
* Pass (no extension, content_type provided):
* {
* "pass": true,
* "file_path": "/absolute/path/to/downloaded_file",
* "file_name": "downloaded_file",
* "file_ext": "",
* "file_size": 123456,
* "media_type": 1,
* "content_type": "application/pdf"
* }
*
* Fail:
* {
* "pass": false,
* "file_path": "/absolute/path/to/video.mp4",
* "file_name": "video.mp4",
* "file_ext": "mp4",
* "reason": "Video files (.mp4) are not supported. ..."
* }
*
* Exit codes:
* 0 = pass — file is ready for upload
* 1 = fail — file rejected (unsupported type, over size limit, etc.)
* 2 = error — file not found, usage error, etc.
*/
const fs = require('node:fs');
const path = require('node:path');
// ─── Extension → media_type + content_type ──────────────────────────────────
const EXT_MAP = {
pdf: { media_type: 1, content_type: 'application/pdf' },
doc: { media_type: 3, content_type: 'application/msword' },
docx: { media_type: 3, content_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
ppt: { media_type: 4, content_type: 'application/vnd.ms-powerpoint' },
pptx: { media_type: 4, content_type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
xls: { media_type: 5, content_type: 'application/vnd.ms-excel' },
xlsx: { media_type: 5, content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
csv: { media_type: 5, content_type: 'text/csv' },
md: { media_type: 7, content_type: 'text/markdown' },
markdown: { media_type: 7, content_type: 'text/markdown' },
png: { media_type: 9, content_type: 'image/png' },
jpg: { media_type: 9, content_type: 'image/jpeg' },
jpeg: { media_type: 9, content_type: 'image/jpeg' },
webp: { media_type: 9, content_type: 'image/webp' },
txt: { media_type: 13, content_type: 'text/plain' },
xmind: { media_type: 14, content_type: 'application/x-xmind' },
mp3: { media_type: 15, content_type: 'audio/mpeg' },
m4a: { media_type: 15, content_type: 'audio/x-m4a' },
wav: { media_type: 15, content_type: 'audio/wav' },
aac: { media_type: 15, content_type: 'audio/aac' },
};
// ─── Content-Type → media_type (reverse lookup) ─────────────────────────────
const CONTENT_TYPE_MAP = {};
for (const [, value] of Object.entries(EXT_MAP)) {
// First entry wins — keeps the canonical content_type per media_type
if (!CONTENT_TYPE_MAP[value.content_type]) {
CONTENT_TYPE_MAP[value.content_type] = value.media_type;
}
}
// Extra aliases not covered by EXT_MAP
Object.assign(CONTENT_TYPE_MAP, {
'text/x-markdown': 7,
'application/md': 7,
'application/markdown': 7,
'application/vnd.xmind.workbook': 14,
'application/zip': 14, // xmind can be zip
});
// ─── Size limits by media_type (bytes) ──────────────────────────────────────
const MB = 1024 * 1024;
const SIZE_LIMITS = {
5: 10 * MB, // Excel / CSV
7: 10 * MB, // Markdown
13: 10 * MB, // TXT
14: 10 * MB, // Xmind
9: 30 * MB, // Image
};
const DEFAULT_SIZE_LIMIT = 200 * MB; // PDF, Word, PPT, Audio, etc.
// ─── Explicitly unsupported extensions ──────────────────────────────────────
const UNSUPPORTED_VIDEO_EXT = new Set(['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv', 'webm', 'm4v', 'rmvb', 'rm', '3gp']);
const UNSUPPORTED_VIDEO_CT = new Set([
'video/mp4',
'video/x-msvideo',
'video/quicktime',
'video/x-matroska',
'video/x-ms-wmv',
'video/x-flv',
'video/webm',
]);
// ─── Helpers ────────────────────────────────────────────────────────────────
function fail(result) {
console.log(JSON.stringify({ pass: false, ...result }));
process.exit(1);
}
function formatSize(bytes) {
if (bytes < MB) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / MB).toFixed(1)} MB`;
}
// ─── Argument parsing ───────────────────────────────────────────────────────
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i++) {
if (argv[i].startsWith('--') && i + 1 < argv.length) {
args[argv[i].replace(/^--/, '')] = argv[i + 1];
i += 1;
}
}
return args;
}
// ─── Main ───────────────────────────────────────────────────────────────────
const args = parseArgs(process.argv);
if (!args.file) {
console.error('Usage: node preflight-check.cjs --file <path> [--content-type <mime>]');
process.exit(2);
}
const filePath = path.resolve(args.file);
const fileName = path.basename(filePath);
const extMatch = fileName.match(/\.([^.]+)$/);
const ext = extMatch ? extMatch[1].toLowerCase() : '';
const inputContentType = args['content-type'] || '';
const base = { file_path: filePath, file_name: fileName, file_ext: ext };
// 1. Check file exists
let stat;
try {
stat = fs.statSync(filePath);
} catch (err) {
if (err.code === 'ENOENT') {
console.error(`File not found: ${filePath}`);
process.exit(2);
}
throw err;
}
// 2. Check not an unsupported video type (by ext or content-type)
if (UNSUPPORTED_VIDEO_EXT.has(ext)) {
fail({ ...base, reason: `Video files (.${ext}) are not supported. Only supported in IMA desktop app.` });
}
if (UNSUPPORTED_VIDEO_CT.has(inputContentType)) {
fail({ ...base, reason: `Video files (${inputContentType}) are not supported. Only supported in IMA desktop app.` });
}
// 3. Resolve media_type and content_type
// Priority: content-type first, then fall back to extension
let mediaType = null;
let contentType = null;
const ctMediaType = inputContentType ? CONTENT_TYPE_MAP[inputContentType] : undefined;
const extMapping = ext ? EXT_MAP[ext] : undefined;
if (ctMediaType != null) {
// Content-type recognized — always wins
mediaType = ctMediaType;
contentType = inputContentType;
} else if (inputContentType) {
// Content-type provided but unrecognized — try extension fallback
if (extMapping) {
mediaType = extMapping.media_type;
contentType = extMapping.content_type;
} else {
fail({
...base,
reason: `Unrecognized content type ${inputContentType}${ext ? ` and file extension .${ext}` : ''}. This file type is not supported.`,
});
}
} else {
// No content-type provided — fall back to extension
if (extMapping) {
mediaType = extMapping.media_type;
contentType = extMapping.content_type;
} else if (ext) {
fail({ ...base, reason: `Unrecognized file extension .${ext}. This file type is not supported.` });
} else {
fail({ ...base, reason: 'File has no extension and no --content-type provided. Cannot determine file type.' });
}
}
// 4. Check file size
const fileSize = stat.size;
const sizeLimit = SIZE_LIMITS[mediaType] || DEFAULT_SIZE_LIMIT;
if (fileSize > sizeLimit) {
fail({
...base,
file_size: fileSize,
media_type: mediaType,
content_type: contentType,
reason: `File size ${formatSize(fileSize)} exceeds the ${formatSize(sizeLimit)} limit for this file type.`,
});
}
// 5. All checks passed
console.log(
JSON.stringify({
pass: true,
...base,
file_size: fileSize,
media_type: mediaType,
content_type: contentType,
}),
);
process.exit(0);
IMA笔记 API
⚠️ 必读约束
🔒 认证
所有请求必须携带 Header:
ima-openapi-clientid: {IMA_OPENAPI_CLIENTID}
ima-openapi-apikey: {IMA_OPENAPI_APIKEY}
Content-Type: application/json🔒 安全规则
- 笔记属于用户隐私,不要在群聊中主动展示笔记内容。
- 仅响应授权用户的笔记操作请求。
---
快速决策
| 用户意图 | 接口别名 |
|---|---|
| 「搜索笔记」「找包含XX的笔记」 | /openapi/note/v1/search_note_book |
| 「列出笔记本」「有哪些笔记本」 | /openapi/note/v1/list_note_folder_by_cursor |
| 「查看XX笔记本里的笔记」 | /openapi/note/v1/list_note_by_folder_id |
| 「从markdown新建笔记」「导入笔记」「创建笔记」「生成笔记」 | /openapi/note/v1/import_doc |
| 「追加内容到笔记」「在笔记末尾添加」 | /openapi/note/v1/append_doc |
| 「获取笔记纯文本」「读取笔记内容」 | /openapi/note/v1/get_doc_content |
---
数据结构
---
DocBasicInfo
| 字段 | 类型 | 说明 |
|---|---|---|
basic_info | DocBasic | 见 DocBasic |
---
DocBasic
| 字段 | 类型 | 说明 |
|---|---|---|
docid | string | 文章 id |
title | string | 标题 |
summary | string | 简介 |
create_time | int64 | |
modify_time | int64 | |
status | DocStatus | 文章状态,0=正常,1=已删除 |
folder_id | string | 文件夹 id |
folder_name | string | 文件夹名称 |
summary_style | map\<string, string\> | 简介样式 |
---
FolderItem(笔记本条目)
list_note_folder_by_cursor 返回的笔记本对象,字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
folder_id | string | 笔记本唯一 ID |
name | string | 笔记本名称 |
note_number | int64 | 笔记本内笔记数量 |
create_time | int64 | 创建时间(Unix 毫秒) |
modify_time | int64 | 修改时间(Unix 毫秒) |
parent_folder_id | string | 上级笔记本 ID(支持嵌套) |
folder_type | int | 类型:0=用户自建,1=全部笔记,2=未分类 |
status | int | 状态:0=正常,1=已删除 |
---
QueryInfo
| 字段 | 类型 | 说明 |
|---|---|---|
title | string | 标题 query |
content | string | 正文 query |
---
SearchedDoc
| 字段 | 类型 | 说明 |
|---|---|---|
doc | DocBasicInfo | 笔记 basic 数据,见 DocBasicInfo |
highlight_info | map\<string, string\> | 该条笔记匹配的高亮词,key: doc_title(文档标题),value: 包含 <em>高亮词</em> 的字段值 |
---
NoteBookFolder
| 字段 | 类型 | 说明 |
|---|---|---|
folder | NoteBookFolderBasicInfo | 笔记本信息,非笔记本为空,见 NoteBookFolderBasicInfo |
---
NoteBookFolderBasicInfo
| 字段 | 类型 | 说明 |
|---|---|---|
basic_info | NoteBookFolderBasic | 见 NoteBookFolderBasic |
---
NoteBookFolderBasic
| 字段 | 类型 | 说明 |
|---|---|---|
folder_id | string | 文件夹 id |
name | string | 笔记本名称 |
status | DocStatus | 笔记本状态,0=正常,1=已删除 |
create_time | int64 | 创建时间 |
modify_time | int64 | 修改时间 |
note_number | int64 | 笔记数量 |
folder_type | FolderType | 文件夹类型:0=用户自建,1=全部笔记,2=未分类 |
---
NoteBookInfo
| 字段 | 类型 | 说明 |
|---|---|---|
basic_info | DocBasicInfo | 笔记基础信息,见 DocBasicInfo |
---
接口详情
1. 搜索笔记
POST /openapi/note/v1/search_note_book
触发场景:用户说「搜索」「找笔记」「查找包含XX的内容」
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
search_type | SearchType | 否 | 检索方式,默认为标题,0=标题,1=正文 |
sort_type | SortType | 否 | 排序方式,默认为更新时间,0=更新时间,1=创建时间,2=标题,3=大小 |
query_info | QueryInfo | 否 | 用户 query,见 QueryInfo |
start | int64 | 是 | 翻页字段 |
end | int64 | 是 | 翻页字段 |
query_id | string | 否 | queryid |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
docs | SearchedDoc[] | 检索到的笔记 list,见 SearchedDoc |
is_end | bool | 是否为最后一批数据 |
total_hit_num | int64 | 检索命中结果总数 |
---
2. 列出笔记本
POST /openapi/note/v1/list_note_folder_by_cursor
触发场景:用户说「列出笔记本」「有哪些分类」「查看笔记本目录」
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
cursor | string | 是 | 游标,第一页传 "0",后续传后台返回的值 |
limit | uint64 | 是 | 获取笔记数量限制 |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
note_book_folders | NoteBookFolder[] | 见 NoteBookFolder |
next_cursor | string | 下次请求的起始游标 |
is_end | bool | 是否为最后一批数据 |
---
3. 按笔记本拉取笔记列表
POST /openapi/note/v1/list_note_by_folder_id
触发场景:用户说「查看XX笔记本的笔记」「列出这个笔记本里的内容」
全部笔记根目录的folder_id为user_list_{userid},可从「列出笔记本」返回的folder_id获取。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
folder_id | string | 否 | 笔记本 ID,根目录为空 |
cursor | string | 是 | 当前游标,首次传空字符串 "" |
limit | uint64 | 是 | 获取笔记数量限制 |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
note_book_list | NoteBookInfo[] | 见 NoteBookInfo |
next_cursor | string | 下次请求的起始游标 |
is_end | bool | 是否为最后一批数据 |
---
4. 从 Markdown 新建笔记
POST /openapi/note/v1/import_doc
触发场景:用户说「从 Markdown 新建笔记」「导入笔记」「把这段 Markdown 保存为笔记」
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
content_format | int | 是 | 文本类型:1=Markdown(默认)目前仅支持 MARKDOWN(值为 1) |
content | string | 是 | 笔记正文内容, 只支持markdown格式 |
folder_id | string | 否 | 关联的笔记本id |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
doc_id | string | 新doc的唯一ID |
---
5. 追加内容到笔记
POST /openapi/note/v1/append_doc
触发场景:用户说「在这篇笔记末尾追加内容」「把 XX 添加到笔记里」
⚠️ 敏感操作:追加会不可撤销地修改已有笔记。如果用户没有明确指定目标笔记(提供doc_id或笔记标题),必须先向用户确认目标笔记,不得自行猜测。模糊场景应优先建议用户使用import_doc新建笔记。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
doc_id | string | 是 | 目标笔记的唯一ID, 需要是本人的笔记 |
content_format | int | 是 | 文本类型:1=Markdown(默认)目前仅支持 MARKDOWN(值为 1) |
content | string | 是 | 要追加的文本内容, 只支持markdown格式 |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
doc_id | string | 目标笔记的唯一ID |
---
6. 获取笔记纯文本
POST /openapi/note/v1/get_doc_content
触发场景:用户说「读取笔记内容」「获取这篇笔记的纯文本」「把笔记转成 Markdown」
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
doc_id | string | 是 | 目标笔记的唯一 ID, 需要是本人的笔记 |
target_content_format | int | 是 | 目标文本类型:0=纯文本(推荐),1=Markdown(不支持),2=JSON |
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
content | string | 笔记的文本内容(按 target_content_format 格式返回) |
---
枚举值
sort_type(排序方式)
| 值 | 说明 |
|---|---|
0 | 更新时间(默认) |
1 | 创建时间 |
2 | 标题 |
3 | 大小 |
search_type(检索方式)
| 值 | 说明 |
|---|---|
0 | 标题检索(默认) |
1 | 正文检索 |
content_format(文本类型)
| 值 | 说明 |
|---|---|
0 | PLAINTEXT - 纯文本 |
1 | MARKDOWN - Markdown 格式 |
2 | JSON - JSON 格式 |
FolderType
| 值 | 说明 |
|---|---|
0 | 用户自建 |
1 | 全部笔记 |
2 | 未分类 |
---
游标翻页使用规范
1. 首次请求:cursor 传空字符串 "" 2. 检查返回的 is_end:false 表示还有更多数据 3. 将返回的 next_cursor 作为下次请求的 cursor 4. is_end = true 时停止翻页
---
错误码
| 错误码 | 说明 |
|---|---|
| 0 | 成功 |
| 100001 | 参数错误 |
| 100002 | 携带无效的 ID |
| 100003 | 服务器内部错误 |
| 100004 | 拉取的 size 不合法(超出范围)/ 用户空间不够 |
| 100005 | 不能获取私有笔记的访客信息 / 不是笔记的作者 |
| 100006 | 笔记已被删除 |
| 100008 | 版本冲突 |
| 100009 | 单篇笔记超过最大限制 |
| 310001 | 笔记本不存在 |
| 20002 | apiKey超过最大限频 |
| 20004 | apikey鉴权失败 |