init
This commit is contained in:
+310
@@ -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';
|
||||
|
||||
/** 策略设置 schema(schemastery 定义,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',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成策略英文 id(slug)
|
||||
* 规则:常见中文策略名映射拼音 → 去空格/转小写/连字符 → 非 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);
|
||||
}
|
||||
Reference in New Issue
Block a user