脚手架上手指南(Scaffolding)
受众:第一次接触 Aalis,想搭建一个可运行的机器人实例,或编写一个可安装的插件的第三方开发者。 本文是入门指引——涵盖两个脚手架各自的用途、运行哪条命令、生成了什么、有哪些约定、下一步往哪走。
相关源码:
packages/create-aalis/src/cli.ts(建项目)、packages/create-aalis-plugin/src/cli.ts(建插件)。 两者都是零运行时依赖、纯 npm/node 的独立脚手架,不属于本 monorepo 的 workspace。
两个脚手架,两件事
Aalis 提供两个互不相干的脚手架,先确认你需要哪个:
| 命令 | 产出 | 何时用 | 入口源码 |
|---|---|---|---|
npm create aalis | 一个可运行的独立项目(机器人实例) | 你要部署 / 跑一个自己的 Aalis bot | create-aalis/src/cli.ts |
create-aalis-plugin | 一个插件骨架包(npm 包) | 你要给 Aalis 写并发布一个扩展插件 | create-aalis-plugin/src/cli.ts |
心智模型:
- 项目 = 一份
aalis.config.yaml+ 一行startAalis()+ 若干装进node_modules的@aalis/plugin-*。运行时从项目package.json的依赖里发现并加载这些插件(node-modules-loader.ts)。 - 插件 = 一个导出
name+apply(ctx, config)的 npm 包(create-aalis-plugin生成src/index.ts),被某个项目装进去后由 core 加载。
两个脚手架遵循同一条外部兼容约定:生成的依赖版本写 "latest"(或解析到的 ^<最新版>),绝不写 workspace:——脚手架产物不在本 monorepo 内,workspace: 协议在外部装不上(create-aalis/cli.ts、create-aalis-plugin/cli.ts)。
一、npm create aalis —— 创建一个机器人项目
Quickstart
前提:Node.js >= 22(与 CI 一致)。
# 交互式:选模板档 + 同类适配器,建好后自动 npm install
npm create aalis@latest my-bot
# 非交互:standard 档 + 各组默认适配器(适合 CI / 管道 / 快速起步)
npm create aalis@latest my-bot -- --yes
# 指定模板档、跳过安装
npm create aalis@latest my-bot -- --tier minimal --no-install注意
--分隔符:npm create后给脚手架的 flag 必须放在--之后,否则会被 npm 自身解析、传不到脚手架。显式写
@latest:不带版本时npm create会直接复用~/.npm/_npx/里已缓存的旧版create-aalis而不检查更新; 0.4 之前的版本经.bin软链调用时会静默退出(exit 0、无输出、不生成任何文件)。
运行后:
cd my-bot
# 若所选插件需要 API key:直接填进 aalis.config.yaml(该文件已在生成的 .gitignore 里)
npm start
# 一次性子命令:等价于聊天里的 /status,执行完即退出(`@aalis/runtime` 默认行为);
# 可用命令取决于装了哪些插件,argv 非空但不是已注册命令时报错退出(exit 2),不会启动守护进程。
# 子命令在独立的一次性进程里执行:status/shutdown/restart 只作用于该临时实例;改配置文件的指令(如 auto)
# 经热重载对守护进程生效,改插件内存态的(如 level、session.*)不生效。管理运行中的实例请用聊天指令 / WebUI
npm start -- status交互式 prompts
无 --yes / --tier 且终端是 TTY 时进交互(非 TTY 环境会提前拦截并提示改用非交互模式,cli.ts)。依次问:
项目目录名 —— 默认
my-aalis-bot,必须是合法 npm 包名(全小写、无空格、不以./_开头等,validateNpmName,cli.ts),非法输入会重问。模板档(默认
standard,cli.ts):档 装什么 bare只装 @aalis/core+@aalis/runtime(完全自定义起点)minimal最简对话闭包:网关 + 指令 + agent + 权限 + 确认通道 + 会话 + 跨会话历史 + 本地存储/进程( MINIMAL_BASE,cli.ts)standardminimal + 常用全家桶:WebUI / 人设 / 向量记忆 / 工具 / 调度 / 技能 / MCP / 联网搜索(Serper,需 key)…( STANDARD_EXTRA,cli.ts)full实时查 npm 全装所有官方插件(可能需手动取舍, cli.ts)同类适配器组(仅
minimal/standard,cli.ts)——避免同类插件同时装入产生冲突,按组选择:- LLM 提供者(多选,默认 DeepSeek —— 需 key;OpenAI 需 key;Ollama 为本机服务、不需要 key)
- 接入平台(多选,默认 CLI 终端)——
standard档的 WebUI 由档位本身带上,此处选不选都会装 - 记忆后端(单选,默认 SQLite)
- Embedding 提供者 / 向量库(仅
standard,向量记忆所需)—— 默认的 OpenAI Embedding 另需一把独立的 key,不想再配就选 Ollama Embedding
序号输入兼容逗号或空格(
"1,2"="1 2"),回车=默认集,非法输入重问(parseIndexSelection,cli.ts)。
--yes 与命令行 flag 的默认值
| flag | 作用 | 默认 |
|---|---|---|
--yes / -y | 跳过所有 prompt,用默认值 | 档=standard,各组取默认成员(cli.ts) |
--tier <档> | 指定模板档(隐含跳过交互) | — |
--no-install | 跳过自动 npm install | 默认会装 |
--force | 目标目录非空时也覆盖写入 | 默认报错退出(cli.ts) |
--registry <url> | 查插件目录/版本用的 npm 源 | https://registry.npmjs.org(cli.ts) |
--registry只影响脚手架查aalis-plugin目录与版本号;生成项目里npm install仍用你自己的 npm 配置(二者解耦,cli.ts)。
生成了什么(项目布局)
my-bot/
├── package.json # @aalis/core + @aalis/runtime + 所选插件(版本见下)
├── index.mjs # 一行 startAalis() 启动
├── aalis.config.yaml # 主配置:name / logLevel / plugins / disabledPlugins(含密钥,**不入库**)
├── .gitignore # node_modules/ data/ *.log aalis.config.yaml dist/
└── README.md # 启动/配置/装更多插件指引入口 index.mjs(renderEntry,cli.ts):
import { startAalis } from '@aalis/runtime';
// 从 aalis.config.yaml 读配置、从 node_modules 加载已装的 @aalis 插件、启动。
startAalis().catch(err => {
console.error('Aalis 启动失败:', err);
process.exit(1);
});startAalis() 是 @aalis/runtime 的总入口(packages/runtime/src/start.ts):读 aalis.config.yaml,用 node_modules 加载器扫项目依赖、按 keywords 含 aalis-plugin 发现插件并加载。
package.json 依赖版本由脚手架逐包实时解析(resolveDepRanges,cli.ts):能查到最新版就写 ^<最新>(与生态约定一致——0.x caret 锁次版本),查不到的回退 "latest"(install 时再取最新,自我修正、不硬编码会过时的版本)。
自动补齐的「伴生」依赖
某些选择会自动带上必需的配套包,避免遗漏:
- 选了 WebUI(
@aalis/plugin-webui-server)→ 自动加@aalis/plugin-webui-client(前端静态资源,缺它 404)+@aalis/plugin-package-manager(市场「安装」否则 503),cli.ts。 - 选了 code_runner(
@aalis/plugin-tool-code-runner)→ 自动加@aalis/plugin-code-sandbox-os(OS 沙箱后端,缺它 fail-closed 拒绝执行),cli.ts。
配置约定
aalis.config.yaml(renderConfig,cli.ts):需要密钥/地址的已知插件会预填一个空的配置桩(如 apiKey: ""),填进去即可;其余用空 plugins: {} 默认配置启动。
装了 plugin-session-manager 时,还会给已选装的 owner 专用平台(cli、webui)各写一条平台档,开放全部工具分组:
plugins:
"@aalis/plugin-session-manager":
platformProfiles:
- platform: cli
enabledToolGroups: ["*"]
- platform: webui
enabledToolGroups: ["*"]带分组的工具(system、skills、scheduler、subtask 等)默认不暴露,平台档列出该组名或写 "*" 才对模型可见。组名要写准——plugin-tool-system 的 shell / 文件 / 系统 / HTTP 工具全部落在 system 这一个组里(见 plugin-tool-system)。之后接入 OneBot 等多人平台时不会自动开组,需要在平台档里按需列出——群成员能驱动哪些 public 工具靠这道闸控制(见 security-model)。
首次启动时 runtime 会把每个已装插件 configSchema 的默认值同步写回该文件(config-sync),几行的初始配置会展开成全量键值——这是预期行为,之后可直接在文件里改任意项;WebUI 配置页与文件双向同步。
密钥直接写在 aalis.config.yaml 里,该文件在生成的 .gitignore 内、不入库。 曾经走 .env + ${VAR} 插值,但它承载的东西与配置文件完全重合,唯一区别只是「哪个文件进 git」;把配置文件本身 ignore 掉之后,那一层就成了多余,已随 ${VAR} 插值一并删除。现在写 ${VAR} 会被原样当作字面量字符串(不再替换),不应再这样写。
生态约定的「更多插件不在终端铺列」:长尾插件发现交给 WebUI 的「插件市场」页(
cli.ts)。装新插件只需npm install @aalis/plugin-<name>,装上即被自动发现加载。
二、create-aalis-plugin —— 创建一个插件骨架
Quickstart
# 交互式
npx create-aalis-plugin
# 指定包名,其余走 prompt
npx create-aalis-plugin my-plugin
# 全默认值(tool 模板,无 command / webui)
npx create-aalis-plugin my-plugin --yes生成后:
cd my-plugin
pnpm install
pnpm build交互式 prompts 与默认值
非 TTY 且无 --yes 时同样提前拦截(cli.ts)。问 4 件事:
| prompt | 默认 | 说明 |
|---|---|---|
| 包名 | aalis-plugin-sample | 合法 npm 包名,支持 @scope/my-plugin;目录名取最后一段(shortName,cli.ts) |
| 显示名(中文标签) | 由包名推导 | 去掉 plugin- 前缀、连字符转空格、首字母大写(defaultDisplayName,cli.ts) |
| 注册 AI 工具? | 是 | 生成 useToolService 工具示例 |
| 注册斜杠命令? | 否 | 生成 useCommandService 命令示例 |
| 提供 WebUI 页面? | 否 | 生成 useWebuiService 页面 + actions 示例 |
--yes / -y 跳过全部,取上表默认(即只生成 tool 扩展点,cli.ts)。yes/no 输入兼容 y/yes/true/1 与 n/no/false/0(parseYesNo,cli.ts)。
生成了什么(插件骨架)
my-plugin/
├── package.json # name / keywords:["aalis-plugin"] / peerDep core / 按选项的 *-api 依赖
├── tsconfig.json # 自包含 compilerOptions(不 extends monorepo base,独立目录也能 tsc)
├── src/index.ts # PluginModule:name / displayName / inject={} / apply()
└── README.md # 启用方式 + 已选扩展点清单生成的 package.json 约定
关键约定(renderPackageJson,cli.ts):
{
"name": "my-plugin",
"type": "module",
"keywords": ["aalis-plugin"], // 市场发现 + 加载硬门(见下)
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"], // 发布包只含编译产物
"dependencies": { "@aalis/api-tools": "latest" }, // 仅当选了对应扩展点
"peerDependencies": { "@aalis/core": ">=0.2.0 <1.0.0" }, // 宽松区间,兼容任何 0.x 宿主
"devDependencies": { "@aalis/core": "latest", "typescript": "^5.7.0", "@types/node": "^22.0.0" }
// 有服务依赖/提供时在此补 aalis.service(注释占位,见下)
}keywords: ["aalis-plugin"]是加载硬门:两个加载器都只认这个关键词来判定「这是不是可加载插件」(isLoadablePlugin,node-modules-loader.ts)。漏了它,插件永远不被发现。@aalis/core走 peerDependency,区间>=0.2.0 <1.0.0:接受任何 0.x 宿主,插件不必随 core 次版本升级重发(别用^0.xcaret 把自己锁死,也别用裸*)。注意 1.0 之前 core 的公开面可能在次版本被删(0.7.0 / 0.9.0 都删过),用了新 API 就把下限抬到对应版本;稳定性承诺自 1.0 起生效,见docs/design/core-contract.md。- 选了哪个扩展点,才把对应
*-api进dependencies:tool→@aalis/api-tools、command→@aalis/api-commands、webui→@aalis/api-webui,统一写"latest"(cli.ts)。 - 不带
aalis字段:示例插件无服务依赖,模板只留一行注释提示往哪写(cli.ts)。一旦你ctx.provide(...)或在inject加依赖,要同步补aalis.service.{provides,required,optional},否则市场「装前披露」会缺项——这两套元数据的对账纪律见 concepts/manifest-metadata.md。
生成的 src/index.ts 形状
入口导出一组 PluginModule 字段(renderIndexTs,cli.ts)。核心元数据:
export const name = 'my-plugin';
export const displayName = 'My Plugin';
export const inject = {}; // 空依赖声明;有 required/optional 服务时在此填
export function apply(ctx: Context, _config: Record<string, unknown>): void {
const logger = ctx.logger.child('my-plugin');
logger.info('插件已加载');
// ...扩展点
}
export const inject = {}是一个显式空依赖声明——没有依赖也写出来,提示你「有依赖往这填」。inject的语义(required参与拓扑排序、optional不参与)见 concepts/manifest-metadata.md 与 concepts/lazy-service-access.md。
选了 tool 时生成的工具模板(cli.ts)——注意这个确切形状:
import { useToolService } from '@aalis/api-tools';
useToolService(ctx).register({
definition: {
type: 'function',
function: {
name: 'hello',
description: '示例工具:返回问候语',
parameters: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
},
},
// handler 返回 string 即纯文本结果;需要把图交给主模型时返回 { content, images }
async handler(args) {
return `你好, ${(args as { name: string }).name}!`;
},
});模板已按正确形状固定,注意两点:
- 工具声明用 OpenAI 函数调用协议的嵌套形状
{ type: 'function', function: { name, description, parameters } }(ToolDefinition,api-tools/src/index.ts),不是平铺的{ name, description }。 handler的返回类型是Promise<string | ToolExecutionResult>(RegisteredTool.handler,api-tools/src/index.ts)——返回字符串即纯文本结果;需要把图片交给主模型亲眼看时返回{ content, images }。
选了 command / webui 时分别追加 useCommandService(ctx).command(...).action(...) 与 useWebuiService(ctx).registerPage(...) 示例(cli.ts)。这些注册 helper 都来自各自的 *-api 包,不来自 core——这也是为什么对应 *-api 要进 dependencies。各扩展点的 helper 一览见 第三方插件开发者指南 第 5 节。
从脚手架到能用的插件
create-aalis-plugin 生成的骨架只是一个能加载、仅打印日志的空壳。让它真正发挥作用需要两步:
1. 提供一个服务
骨架默认只消费(注册工具/命令)。要让别的插件能用你这个能力,在 apply 里 ctx.provide(name, instance, options?),并同步把 provides 写进运行时导出 + package.json 的 aalis.service:
export const provides = ['my-service']; // 源 A:运行时导出(core 读,参与拓扑)
export const inject = { required: ['storage'] };
export function apply(ctx: Context) {
ctx.provide('my-service', new MyService(), { label: 'my-service' });
}// package.json —— 源 B:市场装前披露(webui-server 读)
"aalis": { "service": { "provides": ["my-service"], "required": ["storage"] } }这两套元数据不自动对账,必须手写一致,否则市场「装前/装后」披露漂移。完整规则、真实漂移案例、推荐的 CI 对账见 concepts/manifest-metadata.md。服务的注册/选优/per-entry 多实例语义见 concepts/service-model.md 与 concepts/lazy-service-access.md。
2. 加配置
需要 API key / 地址等参数时,导出 configSchema(WebUI 据此自动渲染表单),在 apply 里读已校验过的 config:
import type { ConfigSchema } from '@aalis/schema-config';
import type {} from '@aalis/api-webui'; // declaration merging:secret 等表单属性
export const configSchema: ConfigSchema = {
apiKey: { type: 'string', label: 'API Key', required: true, secret: true },
};
export function apply(ctx: Context, config: Record<string, unknown>) {
// config 已含 schema 派生默认值(顶层合并)
}配置 schema 的字段归属(secret 等渲染属性来自 webui-api 而非 core)见 第三方插件开发者指南 第 3 节。
3. 本地验证 → 发布
- 本地运行:在 Aalis 项目目录里
npm install ../my-plugin(写进dependencies即被 node_modules 加载器发现)。插件默认启用——plugins段只放配置,启停看顶层disabledPlugins数组,没有enabled开关。放进 monorepopackages/只对自行接了createFsPluginLoader的自托管仓库有效,脚手架生成的项目不走那条路。 - 发布:
npm publish --access public。用户npm install my-plugin后,因keywords含aalis-plugin即被自动发现加载(node-modules-loader.ts)。
完整的「从零到发布」最短路径(消费/提供服务、生命周期 disposable、类型从哪个包 import、参考实现清单)见 第三方插件开发者指南。
下一步
- 启动之后怎么用(要哪些 key / 零 key 走 Ollama / CLI 与 WebUI 两个入口 / 发第一条消息):guide/first-run.md
- 插件包的完整契约与发布流程:guide/third-party-plugin.md
- 两套元数据源(运行时导出 vs
package.jsonaalis.service)与对账纪律:concepts/manifest-metadata.md - 服务模型(provide / getService / 选优 / per-entry):concepts/service-model.md
- 为什么不能缓存服务引用、何时用
whenService:concepts/lazy-service-access.md - 插件作者的安全责任边界:concepts/security-model.md