mm_api 1.1.4 → 1.1.7

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,3 +1,33 @@
1
1
  ## API框架
2
2
  用于实现web api和mvc模式路由请求
3
- 负责配置路由路径、访问权限、校验机制
3
+ 负责配置路由路径、访问权限、校验机制
4
+
5
+ ### 接口数据源(dao.json 的 db 字段)
6
+
7
+ 接口用哪个库由接口自己声明,不依赖路由层注入。在 `dao.json` 里写 `db`:
8
+
9
+ ```json
10
+ {
11
+ "name": "ai_agent",
12
+ "db": "org",
13
+ "schema": "ai.sql",
14
+ "table": "ai_agent",
15
+ ...
16
+ }
17
+ ```
18
+
19
+ - `db` 为数据域名,执行时经 `daoAdmin(域)` 取数据源;省略时默认 `os`(主库)。
20
+ - `db: "org"` 表示按机构分库:机构ID取自 query 参数 `org_id`,取 `daoAdmin('org', { org_id })`,
21
+ 即每个机构一个独立库(`org_{org_id}.db`)。
22
+ - 页面路由 `/ai/{org_id}/` 渲染时不带 query,自动回落到路径中的机构段;两者都没带时使用默认机构 `1`。
23
+ - `org_id` 是数据源标识而非表字段,需在 dao.json 的 `filter` 中声明:
24
+ `"filter": { "org_id": "" }`(值为空表示只抽取不赋值),DAO 生成 where 前会将其抽走(详见 mm_dao 文档)。
25
+ - `schema` 为表结构脚本:首次访问某个机构库时自动执行一次(幂等),之后跳过。
26
+ 路径从 dao.json 所在目录向上逐级查找。
27
+ - 登录态、字典等系统表固定走主库,不受该字段影响。
28
+
29
+ 调用示例:
30
+
31
+ ```
32
+ GET /api/ai/agent?org_id=2&method=get&size=10
33
+ ```
package/lib/api.js CHANGED
@@ -3,6 +3,7 @@ const path = require("path");
3
3
  const Item = require('mm_machine').Mod;
4
4
  const { string, date, object, file } = require("mm_helper");
5
5
  const { getRet } = require('mm_ret');
6
+ const { sqlAdmin } = require('mm_sql');
6
7
  const Oauth = require("./oauth.js"); // 是MM自带的身份验证机制,基于Oauth2.0,可以不使用
7
8
 
8
9
  /**
@@ -23,7 +24,12 @@ const Param = loadOptional("mm_param/lib/param.js"); // 是MM自带的参数机
23
24
  // 回退到 mm_sql 的 Sql(仅支持 run(sql, param, timeout) 原始SQL执行)
24
25
  const mm_dao = loadOptional("mm_dao");
25
26
  const Dao = mm_dao ? mm_dao.Dao : null;
27
+ // 数据域入口:dao.json 声明 db 后,用对应域的数据源执行(未声明时仍为主库 os)
28
+ const daoAdmin = mm_dao ? mm_dao.daoAdmin : null;
26
29
  const Sql = loadOptional("mm_sql/lib/sql.js"); // 是MM自带的sql机制,可以不使用
30
+ // 登录态存储:token 解析优先走 mm_state_store(用户状态)+ mm_dict(token→user_id 映射)
31
+ const mm_state_store = loadOptional("mm_state_store");
32
+ const mm_dict = loadOptional("mm_dict");
27
33
 
28
34
  /** param 实例缓存(模块级,替代原全局 param 模块列表) */
29
35
  const PARAM_CACHE = new Map();
@@ -189,6 +195,8 @@ Api.prototype._preset = function () {
189
195
  this.param = null;
190
196
  // sql模板配置 + 函数
191
197
  this.sql = null;
198
+ // 数据域:接口默认使用主库(os),dao.json 的 db 字段可覆盖为其他域(如 org)
199
+ this.db_scope = this.config.db || "os";
192
200
 
193
201
  var cg = this.config;
194
202
  this._loadParam(cg.param_path);
@@ -328,6 +336,10 @@ Api.prototype._loadSql = function (file_path) {
328
336
  cfg.name = this.config.name || cfg.name;
329
337
  cfg.config_file = p;
330
338
  cfg.dir = path.dirname(p);
339
+ // 数据域:dao.json 用 db 字段声明本接口的数据源(如 "org"),未声明时为主库 os
340
+ this.db_scope = cfg.db || this.db_scope || "os";
341
+ // 表结构脚本:org 域按机构分库时,首次访问该机构库自动建表
342
+ this.schema = this._resolveSchema(cfg.schema);
331
343
  if (Dao) {
332
344
  this.sql = new Dao(cfg, this);
333
345
  } else {
@@ -498,6 +510,146 @@ Api.prototype._getState = async function (db, token) {
498
510
  }
499
511
  };
500
512
 
513
+ /**
514
+ * 解析本次请求的机构ID
515
+ * 机构是调用方指定的数据源标识,以 query 参数 org_id 为准;
516
+ * 页面路由 /ai/{org_id}/ 渲染时不带 query,回落到路径中的机构段;
517
+ * 两者都没有时使用默认机构 1。
518
+ * @param {object} ctx HTTP上下文
519
+ * @returns {number} 机构ID
520
+ * @private
521
+ */
522
+ function _orgId(ctx) {
523
+ var q = (ctx && (ctx.query || (ctx.request && ctx.request.query))) || {};
524
+ var v = q.org_id;
525
+ if (v !== undefined && v !== null && v !== "") {
526
+ var n = parseInt(v, 10);
527
+ if (!isNaN(n) && n > 0) {
528
+ return n;
529
+ }
530
+ }
531
+ var p = (ctx && (ctx.path || (ctx.request && ctx.request.path))) || "";
532
+ var m = /\/ai\/(\d+)(?=\/|$)/.exec(p);
533
+ return m ? parseInt(m[1], 10) : 1;
534
+ }
535
+
536
+ /**
537
+ * 按数据域名取数据管理器
538
+ * 数据源由接口自己声明(dao.json 的 db 字段),不依赖调用方下传:
539
+ * os 为主库;org 域按机构分库(daoAdmin('org', { org_id }));
540
+ * 其余域名交由 mm_dao 的 daoAdmin 解析,域未配置时其内部回落主库。
541
+ * @param {string} scope 数据域名
542
+ * @param {number} [org_id] 机构ID(scope 为 org 时使用)
543
+ * @returns {object} 数据管理器
544
+ * @private
545
+ */
546
+ function _scopedDb(scope, org_id) {
547
+ if (daoAdmin) {
548
+ try {
549
+ if (scope === "org") {
550
+ return daoAdmin("org", { org_id: org_id }).db();
551
+ }
552
+ if (scope && scope !== "os") {
553
+ return daoAdmin(scope).db();
554
+ }
555
+ } catch (err) {
556
+ // 域不可用时退回主库,避免因配置缺失导致接口整体不可用
557
+ }
558
+ }
559
+ return sqlAdmin("os").db();
560
+ }
561
+
562
+ /** 已建表的机构库标记,避免重复执行表结构脚本 */
563
+ const SCHEMA_READY = new Set();
564
+
565
+ /**
566
+ * 解析表结构脚本路径:从接口目录向上逐级查找(最多 8 级),命中即返回
567
+ * @param {string} name 脚本文件名
568
+ * @returns {string|null} 绝对路径,未找到返回 null
569
+ * @private
570
+ */
571
+ Api.prototype._resolveSchema = function (name) {
572
+ if (!name) {
573
+ return null;
574
+ }
575
+ var dir = this.getDir();
576
+ for (var i = 0; i < 8 && dir; i++) {
577
+ var p = file.fullname(name, dir);
578
+ if (fs.existsSync(p)) {
579
+ return p;
580
+ }
581
+ var up = path.dirname(dir);
582
+ if (up === dir) {
583
+ break;
584
+ }
585
+ dir = up;
586
+ }
587
+ return null;
588
+ };
589
+
590
+ /**
591
+ * 首次访问某个机构库时创建表结构(幂等:同一库只执行一次)
592
+ * @param {object} manager 该机构的数据域管理器(提供 load 执行表结构脚本)
593
+ * @param {object} db 数据管理器
594
+ * @returns {Promise<void>} 无返回值
595
+ * @private
596
+ */
597
+ Api.prototype._ensureSchema = async function (manager, db) {
598
+ if (!this.schema || !manager) {
599
+ return;
600
+ }
601
+ var key = (db.database ? db.database() : "") + "|" + this.schema;
602
+ if (SCHEMA_READY.has(key)) {
603
+ return;
604
+ }
605
+ SCHEMA_READY.add(key);
606
+ try {
607
+ await manager.load(this.schema);
608
+ } catch (err) {
609
+ SCHEMA_READY.delete(key);
610
+ throw err;
611
+ }
612
+ };
613
+
614
+ /**
615
+ * 获取本接口的数据管理器(数据源由 dao.json 的 db 字段声明,默认 os)
616
+ * org 域按机构ID分库(org_id 取自 query,页面路径兜底),并在首次访问时建表
617
+ * @param {object} ctx HTTP上下文
618
+ * @returns {Promise<object>} 数据管理器
619
+ * @private
620
+ */
621
+ Api.prototype._getDb = async function (ctx) {
622
+ var scope = this.db_scope || "os";
623
+ if (scope === "org") {
624
+ var org_id = _orgId(ctx);
625
+ var name = "org_" + org_id;
626
+ if (!this._db_manager || this._db_manager_scope !== name) {
627
+ this._org_manager = daoAdmin ? daoAdmin("org", { org_id: org_id }) : null;
628
+ this._db_manager = this._org_manager ? this._org_manager.db() : sqlAdmin("os").db();
629
+ this._db_manager_scope = name;
630
+ }
631
+ await this._ensureSchema(this._org_manager, this._db_manager);
632
+ return this._db_manager;
633
+ }
634
+ if (!this._db_manager || this._db_manager_scope !== scope) {
635
+ this._db_manager = _scopedDb(scope);
636
+ this._db_manager_scope = scope;
637
+ }
638
+ return this._db_manager;
639
+ };
640
+
641
+ /**
642
+ * 获取主库数据管理器(登录态、字典等系统表固定走主库)
643
+ * @returns {object} 数据管理器
644
+ * @private
645
+ */
646
+ Api.prototype._getMainDb = function () {
647
+ if (!this._main_db_manager) {
648
+ this._main_db_manager = sqlAdmin("os").db();
649
+ }
650
+ return this._main_db_manager;
651
+ };
652
+
501
653
  /**
502
654
  * 获取登录态(原名 getState,因与 mm_corejs 生命周期 getState 冲突而更名)
503
655
  * @param {object} ctx 请求上下文
@@ -528,15 +680,37 @@ Api.prototype._getUserFromSession = function (ctx) {
528
680
  return null;
529
681
  };
530
682
 
683
+ /**
684
+ * 从上下文中提取 auth token(协议头优先,Cookie 兜底)
685
+ * @param {object} ctx 请求上下文
686
+ * @returns {string} token,无则返回空串
687
+ * @private
688
+ */
689
+ Api.prototype._getToken = function (ctx) {
690
+ var headers = (ctx && (ctx.headers || (ctx.request && ctx.request.headers))) || {};
691
+ var token = headers[this.config.token_key] || "";
692
+ if (!token && ctx) {
693
+ var cookies = ctx.cookies || (ctx.request && ctx.request.cookies);
694
+ if (cookies) {
695
+ token =
696
+ typeof cookies.get === "function"
697
+ ? cookies.get(this.config.token_key) || ""
698
+ : cookies[this.config.token_key] || "";
699
+ }
700
+ }
701
+ return String(token || "");
702
+ };
703
+
531
704
  /**
532
705
  * 从 token 中获取用户信息
706
+ * 解析顺序:缓存 → 用户状态存储(mm_dict 映射 / 解码 → mm_state_store)→ 数据库
533
707
  * @param {object} ctx 请求上下文
534
708
  * @param {object} db 数据管理器
535
709
  * @returns {object|null} 用户信息
536
710
  * @private
537
711
  */
538
712
  Api.prototype._getUserFromToken = async function (params, ctx, options) {
539
- var token = ctx.headers[this.config.token_key];
713
+ var token = this._getToken(ctx);
540
714
  if (!token) {
541
715
  return null;
542
716
  }
@@ -544,10 +718,76 @@ Api.prototype._getUserFromToken = async function (params, ctx, options) {
544
718
  if (cache) {
545
719
  var u = await cache.get(this.config.session_key + "_" + token);
546
720
  if (u) {
547
- return await this._processUserFromCache(u, ctx, options, token);
721
+ return await this._processUserFromCache(u, ctx, token);
548
722
  }
549
723
  }
550
- return await this._getUserFromDb(token, ctx, options);
724
+ var o = await this._getUserFromState(token, ctx);
725
+ if (o) {
726
+ return o;
727
+ }
728
+ // 数据库兜底:自行获取数据管理器查询
729
+ return await this._getUserFromDb(token, ctx);
730
+ };
731
+
732
+ /**
733
+ * 从用户状态存储获取用户信息
734
+ * token → user_id:mm_dict 映射,或 base64(JSON) 直接解码;
735
+ * user_id → 用户:mm_state_store 的 stateAdmin('user') 中 user_{id} 状态的 user 字段
736
+ * @param {string} token token
737
+ * @param {object} ctx 请求上下文
738
+ * @param {object} options 数据管理器
739
+ * @returns {object|null} 用户信息,未识别或状态中无用户资料返回 null
740
+ * @private
741
+ */
742
+ Api.prototype._getUserFromState = async function (token, ctx, options) {
743
+ if (!mm_state_store || !mm_dict) {
744
+ return null;
745
+ }
746
+ try {
747
+ // 1. token → user_id:字典映射,或 base64(JSON) 解码
748
+ var user_id = null;
749
+ try {
750
+ user_id = mm_dict.dictAdmin("token").get(token);
751
+ } catch (err) {
752
+ user_id = null;
753
+ }
754
+ if (!user_id) {
755
+ try {
756
+ var json = Buffer.from(token, "base64").toString("utf8");
757
+ if (/^\s*\{[\s\S]*\}\s*$/.test(json)) {
758
+ var obj = JSON.parse(json);
759
+ if (obj && obj.user_id !== undefined && obj.user_id !== null && obj.user_id !== "") {
760
+ user_id = String(obj.user_id);
761
+ }
762
+ }
763
+ } catch (err) {
764
+ user_id = null;
765
+ }
766
+ }
767
+ if (!user_id) {
768
+ return null;
769
+ }
770
+ // 2. user_id → 用户状态
771
+ var state = null;
772
+ try {
773
+ state = await mm_state_store.stateAdmin("user").get("user_" + user_id);
774
+ } catch (err) {
775
+ state = null;
776
+ }
777
+ if (!state || !state.user) {
778
+ return null;
779
+ }
780
+ var o = { ...state.user };
781
+ o.token = token;
782
+ o.user_id = String(user_id);
783
+ if (ctx && ctx.session) {
784
+ ctx.session.uuid = ctx.session.uuid || token;
785
+ ctx.session.user = o;
786
+ }
787
+ return o;
788
+ } catch (err) {
789
+ return null;
790
+ }
551
791
  };
552
792
 
553
793
  /**
@@ -559,7 +799,7 @@ Api.prototype._getUserFromToken = async function (params, ctx, options) {
559
799
  * @returns {object|null} 用户信息
560
800
  * @private
561
801
  */
562
- Api.prototype._processUserFromCache = async function (u, ctx, options, token) {
802
+ Api.prototype._processUserFromCache = async function (u, ctx, token) {
563
803
  var o;
564
804
  if (typeof u === "string") {
565
805
  o = { ...string.toJson(u) };
@@ -570,7 +810,7 @@ Api.prototype._processUserFromCache = async function (u, ctx, options, token) {
570
810
  return null;
571
811
  }
572
812
  if (!o.user_id) {
573
- return await this._getUserFromDb(token, ctx, options);
813
+ return await this._getUserFromDb(token, ctx);
574
814
  }
575
815
  o.token = token;
576
816
  return o;
@@ -584,8 +824,8 @@ Api.prototype._processUserFromCache = async function (u, ctx, options, token) {
584
824
  * @returns {object|null} 用户信息
585
825
  * @private
586
826
  */
587
- Api.prototype._getUserFromDb = async function (token, ctx, options) {
588
- var obj = await this._getState(options, token);
827
+ Api.prototype._getUserFromDb = async function (token, ctx) {
828
+ var obj = await this._getState(this._getMainDb(), token);
589
829
  if (obj) {
590
830
  var o = { ...obj.user };
591
831
  ctx.session.user = obj.user;
@@ -633,13 +873,15 @@ Api.prototype.before = async function (params, ctx, options) {
633
873
  Api.prototype.main = async function (params, ctx, options) {
634
874
  if (this.sql) {
635
875
  var req = ctx.request;
636
- // 获取文件
637
- if (req.files) {
876
+ // 获取文件(仅在上传时处理,files 为空对象时不应往 body 写空字段)
877
+ if (req.files && req.files.file) {
638
878
  var fobj = this.saveFile(req.files);
639
879
  req.body.file = fobj.file;
640
880
  req.body.url = fobj.url;
641
881
  }
642
- var ret = await this.sql.run(req.query, req.body, options);
882
+ // 数据源由接口自行解析(dao.json 的 db 字段),
883
+ // 组织过滤由 DAO 按 dao.json 的 filter 声明统一抽取,此处不改动请求参数
884
+ var ret = await this.sql.run(req.query, req.body, await this._getDb(ctx));
643
885
  return ret;
644
886
  } else {
645
887
  return getRet().error(10000, "接口函数未定义");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_api",
3
- "version": "1.1.4",
3
+ "version": "1.1.7",
4
4
  "description": "API框架,用于实现web api和mvc模式路由请求,负责配置路由路径、访问权限、校验机制;基于 mm_corejs,内置管理器池,无全局/原型扩展依赖",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -24,11 +24,13 @@
24
24
  "dependencies": {
25
25
  "mm_corejs": "^1.1.4",
26
26
  "mm_dao": "^1.8.2",
27
+ "mm_dict": "^1.0.3",
27
28
  "mm_helper": "^1.0.0",
28
29
  "mm_machine": "^3.5.4",
29
30
  "mm_param": "^1.1.0",
30
31
  "mm_ret": "^1.4.8",
31
- "mm_sql": "^1.8.0"
32
+ "mm_sql": "^1.8.0",
33
+ "mm_state_store": "^1.0.3"
32
34
  },
33
35
  "scripts": {
34
36
  "test": "node test.js && node --test tests/*.test.js"
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "ai_agent",
3
+ "db": "org",
4
+ "schema": "ai.sql",
5
+ "table": "ai_agent",
6
+ "key": "agent_id",
7
+ "filter": { "org_id": "" },
8
+ "query": {
9
+ "name": "`name` like '%{0}%'"
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "ai_agent_plain",
3
+ "db": "org",
4
+ "table": "ai_agent",
5
+ "key": "agent_id",
6
+ "query": {
7
+ "name": "`name` like '%{0}%'"
8
+ }
9
+ }
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 机构数据源测试
5
+ * @description 覆盖 org 域的机构ID来源(query 优先、路径兜底),以及
6
+ * 控制参数在进入 where 之前被剔除(DAO 层统一处理)
7
+ */
8
+
9
+ const test = require('node:test');
10
+ const assert = require('node:assert/strict');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+ const Api = require('../lib/api.js');
14
+ const { daoAdmin } = require('mm_dao');
15
+
16
+ // 登记 org 域的分库模板(每个机构一个独立的 SQLite 库)
17
+ daoAdmin('org', {
18
+ sql: {
19
+ way: 'sqlite',
20
+ sqlite: { dir: path.join(os.tmpdir(), 'mm_api_org_test/'), database: 'org_{org_id}' }
21
+ }
22
+ });
23
+
24
+ /**
25
+ * 构造接口实例
26
+ * @param {string} dao_file dao 配置文件名
27
+ * @returns {Api} 接口实例
28
+ */
29
+ function makeApi(dao_file) {
30
+ const api = new Api({
31
+ name: 'org_probe_' + Math.random().toString(36).slice(2),
32
+ dir: __dirname,
33
+ db: 'org'
34
+ });
35
+ api._loadSql('fixtures/' + dao_file);
36
+ return api;
37
+ }
38
+
39
+ /**
40
+ * 拦截 DAO 执行,记录实际使用的库与查询参数
41
+ * @param {Api} api 接口实例
42
+ * @returns {object} 记录对象
43
+ */
44
+ function spy(api) {
45
+ const seen = { db: '', query: null };
46
+ api.sql.run = async (query, body, db) => {
47
+ seen.db = String(db.database());
48
+ seen.query = { ...query };
49
+ return { error: 0 };
50
+ };
51
+ return seen;
52
+ }
53
+
54
+ test('org_id 取自 query 参数', async () => {
55
+ const api = makeApi('dao_org.json');
56
+ const seen = spy(api);
57
+ await api.main({}, { request: { query: { org_id: '2', method: 'get' }, body: {} }, path: '/api/ai/agent' }, {});
58
+ assert.equal(seen.db, 'org_2', '应落到机构 2 的库');
59
+ });
60
+
61
+ test('页面路由不带 query 时回落路径中的机构段', async () => {
62
+ const api = makeApi('dao_org.json');
63
+ const seen = spy(api);
64
+ await api.main({}, { request: { query: { method: 'get' }, body: {} }, path: '/ai/3/' }, {});
65
+ assert.equal(seen.db, 'org_3', '应落到机构 3 的库');
66
+ });
67
+
68
+ test('query 与路径同时存在时以 query 为准', async () => {
69
+ const api = makeApi('dao_org.json');
70
+ const seen = spy(api);
71
+ await api.main({}, { request: { query: { org_id: '5', method: 'get' }, body: {} }, path: '/ai/9/' }, {});
72
+ assert.equal(seen.db, 'org_5', 'query 优先于路径');
73
+ });
74
+
75
+ test('都未指定时使用默认机构 1', async () => {
76
+ const api = makeApi('dao_org.json');
77
+ const seen = spy(api);
78
+ await api.main({}, { request: { query: { method: 'get' }, body: {} }, path: '/api/ai/agent' }, {});
79
+ assert.equal(seen.db, 'org_1', '默认落到机构 1');
80
+ });
81
+
82
+ test('dao.json 声明 filter 后 org_id 不进入 where', () => {
83
+ const api = makeApi('dao_org.json');
84
+ const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
85
+ const where = api.sql.toWhere(db, { method: 'get', org_id: '2' });
86
+ assert.equal(where, '', 'org_id 应被抽取,where 为空');
87
+ });
88
+
89
+ test('未声明 filter 时 org_id 会被当成查询条件', () => {
90
+ const api = makeApi('dao_org_plain.json');
91
+ const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
92
+ const where = api.sql.toWhere(db, { method: 'get', org_id: '2' });
93
+ assert.equal(where, "`org_id` = '2'", '未声明时按普通字段处理');
94
+ });
95
+
96
+ test('控制参数不进入 where(含假值)', () => {
97
+ const api = makeApi('dao_org.json');
98
+ const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
99
+ const where = api.sql.toWhere(db, {
100
+ method: 'get',
101
+ page: '0',
102
+ size: '',
103
+ orderby: 'agent_id desc',
104
+ db: 'org',
105
+ groupby: 'llm',
106
+ org_id: '2'
107
+ });
108
+ assert.equal(where, '', '分页、排序、数据域等控制参数都应被剔除');
109
+ });
110
+
111
+ test('业务查询条件正常保留', () => {
112
+ const api = makeApi('dao_org.json');
113
+ const db = daoAdmin('org', { org_id: 2 }).db().new('ai_agent', 'agent_id');
114
+ const where = api.sql.toWhere(db, { method: 'get', org_id: '2', name: '张三' });
115
+ assert.equal(where, "`name` like '%张三%'", '业务字段应按 query 模板生成条件');
116
+ });
117
+
118
+ test('非 org 域不受 org_id 影响', () => {
119
+ const api = new Api({ name: 'os_probe_' + Math.random().toString(36).slice(2), dir: __dirname });
120
+ assert.equal(api.db_scope, 'os', '未声明 db 时为主库');
121
+ });