This commit is contained in:
2026-08-29 16:20:33 +08:00
commit cba31428dc
52 changed files with 6553 additions and 0 deletions
+310
View File
@@ -0,0 +1,310 @@
/**
* 设置管理:策略(标签)配置
*
* 设计约束:产品约束-002/003/004(标签体系、标签自定义 + 显示开关、删除份额回未分配)
* 通过 ctx.settings.register('one-divine-lot', schema) 注册策略配置 namespace。
*
* 数据结构:
* strategies: [{ id, name, visible, order }]
* 预置:网格超市、手动做T(R-003 O3-5:可删除,彻底自由)
*
* 迭代 02 新增(R-003 O3 策略 CRUD):
* - generateStrategyId(name):中文/任意名 → 英文 slug id
* - addStrategy / renameStrategy / moveStrategy / removeStrategy
* - removeStrategy 的份额清理由上层(api.js 编排 storage)完成
*/
import z from '@deepseek-ai/schemastery';
/** 策略设置 schemaschemastery 定义,settings.register 要求 schema 对象) */
export const strategySchema = z.object({
strategies: z.array(z.object({
id: z.string().required(),
name: z.string().required(),
visible: z.boolean().default(true),
order: z.number().default(0),
})).default([
{ id: 'grid-supermarket', name: '网格超市', visible: true, order: 0 },
{ id: 'manual-t', name: '手动做T', visible: true, order: 1 },
]),
// 通用 tab 显隐配置(设置 tab 化扩展,2026-08-28
tabs: z.object({
allPositions: z.boolean().default(true), // 全部持仓
tradeRecords: z.boolean().default(true), // 交易记录(未来 tab,本期空占位)
watchlist: z.boolean().default(true), // 关注列表(未来 tab,本期空占位)
}).default({
allPositions: true,
tradeRecords: true,
watchlist: true,
}),
});
/** 默认通用 tab 配置 */
export const DEFAULT_TABS = {
allPositions: true,
tradeRecords: true,
watchlist: true,
};
/** 默认策略(首次注册时的初始值) */
export const DEFAULT_STRATEGIES = [
{ id: 'grid-supermarket', name: '网格超市', visible: true, order: 0 },
{ id: 'manual-t', name: '手动做T', visible: true, order: 1 },
];
/** 简易拼音映射表(常用汉字 → 拼音,用于 slug 生成兜底) */
const PINYIN_MAP = {
// 完整短语映射(优先级最高)
'网格超市': 'grid-supermarket',
'手动做T': 'manual-t',
'长线持有': 'long-term-hold',
'短线交易': 'short-term-trade',
'波段操作': 'swing-trade',
'价值投资': 'value-invest',
'趋势跟踪': 'trend-follow',
'新股申购': 'ipo-subscribe',
'打板': 'board',
'低吸': 'dip-buy',
'高抛': 'high-sell',
'做T': 't',
// 词映射
'长线': 'long-term',
'短线': 'short-term',
'持有': 'hold',
'波段': 'swing',
'价值': 'value',
'网格': 'grid',
'策略': 'strategy',
'趋势': 'trend',
'新股': 'new-stock',
'打新': 'ipo',
// 单字映射
'超': 'super',
'市': 'market',
'场': 'market',
'手': 'manual',
'动': 'action',
'长': 'long',
'短': 'short',
'持': 'hold',
'波': 'swing',
'值': 'value',
'格': 'grid',
'略': 'strategy',
'趋': 'trend',
'势': 'trend',
};
/**
* 生成策略英文 idslug
* 规则:常见中文策略名映射拼音 → 去空格/转小写/连字符 → 非 ASCII 字符用序号兜底
* @param {string} name 策略名称
* @param {string[]} existingIds 已存在的 id(用于去重)
* @returns {string} 唯一英文 id
*/
export function generateStrategyId(name, existingIds = []) {
const raw = String(name ?? '').trim();
if (!raw) return '';
// 1. 整名映射(常见策略名直接映射,优先级最高)
const whole = PINYIN_MAP[raw];
if (whole) return dedupe(whole, existingIds);
// 2. 最长子串贪婪匹配(把名字切成已映射的词/字片段)
let slug = '';
let i = 0;
while (i < raw.length) {
let matched = false;
for (let len = Math.min(4, raw.length - i); len >= 1; len--) {
const seg = raw.slice(i, i + len);
const p = PINYIN_MAP[seg];
if (p) {
slug += p + '-';
i += len;
matched = true;
break;
}
}
if (matched) continue;
const ch = raw[i];
if (/[a-zA-Z0-9]/.test(ch)) {
slug += ch.toLowerCase() + '-';
}
i++;
}
let base = slug.replace(/-+$/, '').replace(/^-+/, '') || 'strategy';
// 3. 规范化(去空格、连字符规整、移除非法字符)
base = base
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
|| 'strategy';
return dedupe(base, existingIds);
}
/** 去重:若 id 已存在,追加序号 */
function dedupe(base, existingIds) {
let candidate = base;
let i = 2;
while (existingIds.includes(candidate)) {
candidate = `${base}-${i}`;
i++;
}
return candidate;
}
/**
* 注册策略设置 namespace,返回 SettingsScope
* @param {import('@deepseek-ai/cordis').Context} ctx
*/
export function registerSettings(ctx, config) {
const ns = config.settingsNamespace ?? 'one-divine-lot';
const scope = ctx.settings.register(ns, strategySchema, {
// 组合 base(如需从 composition 注入默认值)
base: config.strategies ? { strategies: config.strategies } : undefined,
});
ctx.logger?.info(`[one-divine-lot] 设置已注册: ${ns}`);
return scope;
}
/**
* 读取策略列表(含 visible 过滤)
* @param {ReturnType<typeof registerSettings>} scope
*/
export function getStrategies(scope) {
const value = scope?.get() ?? {};
const list = Array.isArray(value.strategies) ? value.strategies : DEFAULT_STRATEGIES;
// 统一按 order 排序(设置页 / tab 栏 / 逻辑层共用同一顺序)
return list.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
/**
* 获取可见策略列表(visible === true,按 order 排序)
*/
export function getVisibleStrategies(scope) {
return getStrategies(scope)
.filter((s) => s.visible !== false)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
/**
* 更新策略(整表更新)
* @param {ReturnType<typeof registerSettings>} scope
* @param {Array} strategies 完整策略列表
*/
export async function updateStrategies(scope, strategies) {
await scope.update({ strategies });
}
/**
* 新增策略(O3
* @param {ReturnType<typeof registerSettings>} scope
* @param {string} name 策略名称
* @returns {Promise<object>} 新策略对象
*/
export async function addStrategy(scope, name) {
const list = getStrategies(scope);
const id = generateStrategyId(name, list.map((s) => s.id));
const strategy = {
id,
name: String(name ?? '').trim(),
visible: true,
order: list.reduce((max, s) => Math.max(max, s.order ?? 0), -1) + 1,
};
await updateStrategies(scope, [...list, strategy]);
return strategy;
}
/**
* 重命名策略(O3;分配数据挂 id 不挂名称,改名不影响)
* @param {ReturnType<typeof registerSettings>} scope
* @param {string} id 策略 id
* @param {string} name 新名称
* @returns {Promise<boolean>} 是否成功
*/
export async function renameStrategy(scope, id, name) {
const list = getStrategies(scope);
const target = list.find((s) => s.id === id);
if (!target) throw Object.assign(new Error(`策略不存在: ${id}`), { code: 'strategy-not-found' });
const next = list.map((s) => (s.id === id ? { ...s, name: String(name ?? '').trim() || s.name } : s));
await updateStrategies(scope, next);
return true;
}
/**
* 移动策略(排序,上移/下移)
* @param {ReturnType<typeof registerSettings>} scope
* @param {string} id 策略 id
* @param {'up'|'down'} dir 方向
* @returns {Promise<boolean>} 是否成功
*/
export async function moveStrategy(scope, id, dir) {
const list = getStrategies(scope)
.slice()
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
const idx = list.findIndex((s) => s.id === id);
if (idx < 0) throw Object.assign(new Error(`策略不存在: ${id}`), { code: 'strategy-not-found' });
const swapIdx = dir === 'up' ? idx - 1 : idx + 1;
if (swapIdx < 0 || swapIdx >= list.length) return false; // 已在边界
const a = list[idx];
const b = list[swapIdx];
const orderA = a.order ?? 0;
const orderB = b.order ?? 0;
const next = list.map((s) => {
if (s.id === a.id) return { ...s, order: orderB };
if (s.id === b.id) return { ...s, order: orderA };
return s;
});
await updateStrategies(scope, next);
return true;
}
/**
* 删除策略(O3;份额清理由上层编排 storage)
* @param {ReturnType<typeof registerSettings>} scope
* @param {string} id 策略 id
* @returns {Promise<boolean>} 是否成功
*/
export async function removeStrategy(scope, id) {
const list = getStrategies(scope);
const target = list.find((s) => s.id === id);
if (!target) throw Object.assign(new Error(`策略不存在: ${id}`), { code: 'strategy-not-found' });
await updateStrategies(scope, list.filter((s) => s.id !== id));
return true;
}
/**
* 读取通用 tab 显隐配置(设置 tab 化扩展)
* @param {ReturnType<typeof registerSettings>} scope
* @returns {{ allPositions: boolean, tradeRecords: boolean, watchlist: boolean }}
*/
export function getTabConfig(scope) {
const value = scope?.get() ?? {};
const tabs = value.tabs ?? {};
return {
allPositions: tabs.allPositions !== false,
tradeRecords: tabs.tradeRecords !== false,
watchlist: tabs.watchlist !== false,
};
}
/**
* 更新通用 tab 显隐配置(整表更新)
* @param {ReturnType<typeof registerSettings>} scope
* @param {object} tabs { allPositions?, tradeRecords?, watchlist? }
*/
export async function updateTabConfig(scope, tabs) {
const current = scope?.get() ?? {};
const next = {
...current,
tabs: {
allPositions: tabs.allPositions !== false,
tradeRecords: tabs.tradeRecords !== false,
watchlist: tabs.watchlist !== false,
},
};
await scope.update(next);
}