初始化项目
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||
*/
|
||||
export const HAR_VERSION = '1.0.0';
|
||||
export const BUILD_MODE_NAME = 'debug';
|
||||
export const DEBUG = true;
|
||||
export const TARGET_NAME = 'default';
|
||||
|
||||
/**
|
||||
* BuildProfile Class is used only for compatibility purposes.
|
||||
*/
|
||||
export default class BuildProfile {
|
||||
static readonly HAR_VERSION = HAR_VERSION;
|
||||
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||
static readonly DEBUG = DEBUG;
|
||||
static readonly TARGET_NAME = TARGET_NAME;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @kuge/core 的唯一公开入口。
|
||||
*
|
||||
* 约定(见 docs/adr/ADR-001):HAR 模块只通过本文件暴露能力,
|
||||
* 外部模块禁止直接 import src/main/ets 下的实现文件。
|
||||
*/
|
||||
export { Logger, LogLevel, LOG_DOMAIN, LogSink } from './src/main/ets/logger/Logger';
|
||||
export { FileLogSink } from './src/main/ets/logger/FileLogSink';
|
||||
export { AppConfig } from './src/main/ets/config/AppConfig';
|
||||
export { AppInfo } from './src/main/ets/app/AppInfo';
|
||||
export { TaskScheduler, TaskRunner, TaskCancelledError } from './src/main/ets/task/TaskScheduler';
|
||||
export { LruCache } from './src/main/ets/storage/LruCache';
|
||||
export { KeyValueStore, MemoryKeyValueStore } from './src/main/ets/storage/KeyValueStore';
|
||||
export { CredentialStore, CredentialVault, KeyProvider, InMemoryKeyProvider } from './src/main/ets/security/CredentialVault';
|
||||
export { I18nUtil } from './src/main/ets/i18n/I18n';
|
||||
export { DiagnosticBuilder, DiagnosticContext, Redactor } from './src/main/ets/diagnostics/DiagnosticBuilder';
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {},
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@kuge/core",
|
||||
"version": "1.0.0",
|
||||
"description": "Kuge core capabilities: logging, app info, shared types",
|
||||
"main": "Index.ets",
|
||||
"author": "Kuge Team",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 应用级静态信息。
|
||||
*
|
||||
* 仅存放不随构建变化的产品标识;构建期注入的信息(版本号、构建号)
|
||||
* 由 AppScope/app.json5 与 BuildProfile 提供,不在此重复维护。
|
||||
*/
|
||||
export class AppInfo {
|
||||
/** 产品中文名。 */
|
||||
static readonly NAME: string = '库阁';
|
||||
/** 产品英文名(拼音音译)。 */
|
||||
static readonly ENGLISH_NAME: string = 'Kuge';
|
||||
/** 一句话定位。 */
|
||||
static readonly TAGLINE: string = '跨库数据库工作台';
|
||||
/** 目标平台。 */
|
||||
static readonly PLATFORM: string = 'HarmonyOS NEXT';
|
||||
/** 目标设备形态。 */
|
||||
static readonly FORM_FACTORS: string = 'PC / 平板';
|
||||
/** 当前迭代版本(与 app.json5 的 versionName 保持同步)。 */
|
||||
static readonly VERSION: string = '0.1.0';
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 应用配置(持久化)。
|
||||
*
|
||||
* 使用 Preferences 保存少量启动即读的配置项;业务级配置(连接、外观等)
|
||||
* 在后续工作包中按领域拆到各自模块,不堆在同一个 key 空间里。
|
||||
*
|
||||
* 约定:配置读写同步完成(Preferences 的 Sync 接口),避免启动阶段异步竞态;
|
||||
* 落盘用 flush() 异步提交,不阻塞 UI 线程。存储不可用时退化为内存默认值,
|
||||
* 不阻断应用启动。
|
||||
*/
|
||||
|
||||
import { preferences } from '@kit.ArkData';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { LogLevel } from '../logger/Logger';
|
||||
|
||||
/** Preferences 文件名。 */
|
||||
const STORE_NAME: string = 'kuge_settings';
|
||||
/** 日志级别键。 */
|
||||
const KEY_LOG_LEVEL: string = 'log.level';
|
||||
|
||||
export class AppConfig {
|
||||
private static store: preferences.Preferences | undefined = undefined;
|
||||
private static logLevel: LogLevel = LogLevel.INFO;
|
||||
|
||||
/** 在 Ability 启动时调用一次;重复调用无副作用。 */
|
||||
static init(context: common.Context): void {
|
||||
if (AppConfig.store !== undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const options: preferences.Options = { name: STORE_NAME };
|
||||
const store: preferences.Preferences = preferences.getPreferencesSync(context, options);
|
||||
const raw: preferences.ValueType = store.getSync(KEY_LOG_LEVEL, 'info');
|
||||
AppConfig.store = store;
|
||||
AppConfig.logLevel = AppConfig.parseLevel(typeof raw === 'string' ? raw : 'info');
|
||||
} catch (err) {
|
||||
// 存储不可用(首次启动异常、配额受限等)时使用默认值,不影响主流程。
|
||||
AppConfig.store = undefined;
|
||||
AppConfig.logLevel = LogLevel.INFO;
|
||||
}
|
||||
}
|
||||
|
||||
static getLogLevel(): LogLevel {
|
||||
return AppConfig.logLevel;
|
||||
}
|
||||
|
||||
/** 修改并持久化日志级别。 */
|
||||
static setLogLevel(level: LogLevel): void {
|
||||
AppConfig.logLevel = level;
|
||||
const store = AppConfig.store;
|
||||
if (store === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
store.putSync(KEY_LOG_LEVEL, AppConfig.levelName(level));
|
||||
store.flush();
|
||||
} catch (err) {
|
||||
// 持久化失败时保留内存值,下次启动回落到旧值。
|
||||
}
|
||||
}
|
||||
|
||||
/** 级别名(用于持久化与界面展示)。 */
|
||||
static levelName(level: LogLevel): string {
|
||||
switch (level) {
|
||||
case LogLevel.DEBUG:
|
||||
return 'debug';
|
||||
case LogLevel.INFO:
|
||||
return 'info';
|
||||
case LogLevel.WARN:
|
||||
return 'warn';
|
||||
case LogLevel.ERROR:
|
||||
return 'error';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
|
||||
private static parseLevel(raw: string): LogLevel {
|
||||
switch (raw) {
|
||||
case 'debug':
|
||||
return LogLevel.DEBUG;
|
||||
case 'warn':
|
||||
return LogLevel.WARN;
|
||||
case 'error':
|
||||
return LogLevel.ERROR;
|
||||
case 'info':
|
||||
default:
|
||||
return LogLevel.INFO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 诊断信息构建与脱敏(1.3.10)。
|
||||
*
|
||||
* 职责:
|
||||
* 1. 汇总应用/平台/版本/时间与日志尾部,生成可读的诊断文本;
|
||||
* 2. 对敏感字段做脱敏(密码、令牌、密钥、鉴权头),保证诊断包可安全外发;
|
||||
* 3. 崩溃处理与 faultlogger 接入在平台适配器中完成,本文件保持纯逻辑、可测试。
|
||||
*/
|
||||
|
||||
/** 诊断上下文。 */
|
||||
export interface DiagnosticContext {
|
||||
appName: string;
|
||||
version: string;
|
||||
platform: string;
|
||||
formFactors: string;
|
||||
logLevel: string;
|
||||
}
|
||||
|
||||
/** 脱敏器:按常见敏感键名替换值。 */
|
||||
export class Redactor {
|
||||
/** 需要脱敏的键名(小写比较)。 */
|
||||
private static readonly KEYS: string[] = [
|
||||
'password', 'passwd', 'pwd', 'secret', 'token', 'apikey', 'api_key', 'authorization', 'credential'
|
||||
];
|
||||
|
||||
/** 把 key=value / key: value 形式的值替换为 ***。 */
|
||||
static redact(text: string): string {
|
||||
let out: string = text;
|
||||
for (let i = 0; i < Redactor.KEYS.length; i++) {
|
||||
const key: string = Redactor.KEYS[i];
|
||||
const pattern: RegExp = new RegExp('(' + key + '\\s*[=:]\\s*)([^\\s,;]+)', 'gi');
|
||||
out = out.replace(pattern, '$1***');
|
||||
}
|
||||
// 鉴权头整行脱敏(可能含 "Bearer xxx" 等前缀)
|
||||
out = out.replace(new RegExp('(authorization\\s*[=:]\\s*)[^\\r\\n]*', 'gi'), '$1***');
|
||||
// 裸 Bearer 令牌
|
||||
out = out.replace(new RegExp('(bearer\\s+)[A-Za-z0-9._\\-]+', 'gi'), '$1***');
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/** 诊断包构建器。 */
|
||||
export class DiagnosticBuilder {
|
||||
private readonly ctx: DiagnosticContext;
|
||||
|
||||
constructor(ctx: DiagnosticContext) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成诊断文本。
|
||||
* @param logTail 日志尾部(已读取的内容)
|
||||
* @param timestamp ISO 时间字符串,由调用方提供,避免依赖本地时区
|
||||
*/
|
||||
build(logTail: string, timestamp: string): string {
|
||||
const lines: string[] = [];
|
||||
lines.push('=== 库阁诊断信息 ===');
|
||||
lines.push('应用: ' + this.ctx.appName + ' ' + this.ctx.version);
|
||||
lines.push('平台: ' + this.ctx.platform + ' (' + this.ctx.formFactors + ')');
|
||||
lines.push('日志级别: ' + this.ctx.logLevel);
|
||||
lines.push('时间: ' + timestamp);
|
||||
lines.push('--- 日志尾部 ---');
|
||||
lines.push(Redactor.redact(logTail));
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 多语言运行时(1.3.9)。
|
||||
*
|
||||
* 纯逻辑实现:语言标签归一化、支持列表匹配、用户偏好与系统语言的协商。
|
||||
* 资源文件仍由 resources/<语言>/element/string.json 提供,本类只决定「用哪个语言」。
|
||||
*
|
||||
* 协商顺序(对应 I18N-001 设计):
|
||||
* 1) 用户显式选择且在支持列表内 → 用它;
|
||||
* 2) 否则取系统语言,按「完全匹配 → 主语言匹配」找支持项;
|
||||
* 3) 都不匹配 → 回退 fallback(中文简体)。
|
||||
*
|
||||
* 标签规范:小写化并用 '-' 分隔,如 zh-Hans-CN、en-US;忽略大小写与下划线差异。
|
||||
*/
|
||||
export class I18nUtil {
|
||||
/** 归一化语言标签:小写、下划线转连字符、去空白。 */
|
||||
static normalize(tag: string): string {
|
||||
return tag.trim().toLowerCase().replace('_', '-');
|
||||
}
|
||||
|
||||
/** 主语言(第一个子标签),如 zh-Hans-CN → zh。 */
|
||||
static primaryOf(tag: string): string {
|
||||
const normalized: string = I18nUtil.normalize(tag);
|
||||
const idx: number = normalized.indexOf('-');
|
||||
return idx < 0 ? normalized : normalized.substring(0, idx);
|
||||
}
|
||||
|
||||
/** 是否在支持列表中(完全匹配,忽略大小写)。 */
|
||||
static isSupported(tag: string, supported: string[]): boolean {
|
||||
const target: string = I18nUtil.normalize(tag);
|
||||
for (let i = 0; i < supported.length; i++) {
|
||||
if (I18nUtil.normalize(supported[i]) === target) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 在支持列表中找到与给定标签最匹配的项;找不到返回 undefined。 */
|
||||
static match(tag: string, supported: string[]): string | undefined {
|
||||
const target: string = I18nUtil.normalize(tag);
|
||||
for (let i = 0; i < supported.length; i++) {
|
||||
if (I18nUtil.normalize(supported[i]) === target) {
|
||||
return supported[i];
|
||||
}
|
||||
}
|
||||
const primary: string = I18nUtil.primaryOf(target);
|
||||
for (let i = 0; i < supported.length; i++) {
|
||||
if (I18nUtil.primaryOf(supported[i]) === primary) {
|
||||
return supported[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协商最终语言。
|
||||
* @param systemTag 系统语言,如 zh-Hans-CN
|
||||
* @param userTag 用户偏好(空表示跟随系统)
|
||||
* @param supported 支持列表,顺序即优先级
|
||||
* @param fallback 回退语言
|
||||
*/
|
||||
static negotiate(systemTag: string, userTag: string, supported: string[], fallback: string): string {
|
||||
if (userTag.length > 0) {
|
||||
const byUser: string | undefined = I18nUtil.match(userTag, supported);
|
||||
if (byUser !== undefined) {
|
||||
return byUser;
|
||||
}
|
||||
}
|
||||
if (systemTag.length > 0) {
|
||||
const bySystem: string | undefined = I18nUtil.match(systemTag, supported);
|
||||
if (bySystem !== undefined) {
|
||||
return bySystem;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 文件日志落地。
|
||||
*
|
||||
* 只把 WARN 及以上写入文件,控制 IO 量与磁盘占用;完整日志仍在 hilog。
|
||||
* 单文件超过上限时截断重写(保留最近一个窗口),后续接入归档与诊断包导出
|
||||
* 时再替换为滚动策略(对应 1.3.10 崩溃上报与诊断打包)。
|
||||
*
|
||||
* 所有文件操作都在内部吞掉异常:日志系统自身的失败不得影响业务。
|
||||
*/
|
||||
|
||||
import { fileIo as fs } from '@kit.CoreFileKit';
|
||||
import { LogLevel, LogSink } from './Logger';
|
||||
|
||||
/** 单文件上限:512 KB。 */
|
||||
const MAX_FILE_BYTES: number = 512 * 1024;
|
||||
|
||||
export class FileLogSink implements LogSink {
|
||||
private readonly filePath: string;
|
||||
private readonly dirPath: string;
|
||||
private file: fs.File | undefined = undefined;
|
||||
|
||||
constructor(dirPath: string) {
|
||||
this.dirPath = dirPath;
|
||||
this.filePath = dirPath + '/kuge.log';
|
||||
try {
|
||||
if (!fs.accessSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath);
|
||||
}
|
||||
} catch (err) {
|
||||
// 目录创建失败时仍尝试直接打开文件(目录可能已存在)。
|
||||
}
|
||||
this.file = this.openAppendSafe();
|
||||
}
|
||||
|
||||
/** 只落 WARN / ERROR,避免 DEBUG 日志把文件刷满。 */
|
||||
write(level: LogLevel, tag: string, message: string): void {
|
||||
if (level < LogLevel.WARN) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.rotateIfNeeded();
|
||||
const file = this.file;
|
||||
if (file === undefined) {
|
||||
return;
|
||||
}
|
||||
const line: string = this.timestamp() + ' [' + this.levelTag(level) + '] [' + tag + '] ' + message + '\n';
|
||||
fs.writeSync(file.fd, line);
|
||||
} catch (err) {
|
||||
// 忽略写入失败,避免日志系统本身造成业务中断。
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取最近 maxLines 行,用于诊断面板与问题反馈。 */
|
||||
readTail(maxLines: number): string {
|
||||
try {
|
||||
const all: string = fs.readTextSync(this.filePath);
|
||||
const lines: string[] = all.split('\n');
|
||||
const start: number = Math.max(0, lines.length - maxLines);
|
||||
return lines.slice(start).join('\n');
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
const file = this.file;
|
||||
if (file === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.closeSync(file);
|
||||
} catch (err) {
|
||||
// 忽略关闭异常
|
||||
}
|
||||
this.file = undefined;
|
||||
}
|
||||
|
||||
private openAppendSafe(): fs.File | undefined {
|
||||
try {
|
||||
return fs.openSync(this.filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.APPEND);
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private rotateIfNeeded(): void {
|
||||
const file = this.file;
|
||||
if (file === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const size: number = fs.statSync(this.filePath).size;
|
||||
if (size <= MAX_FILE_BYTES) {
|
||||
return;
|
||||
}
|
||||
fs.closeSync(file);
|
||||
this.file = undefined;
|
||||
const fresh: fs.File = fs.openSync(this.filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
|
||||
fs.closeSync(fresh);
|
||||
this.file = this.openAppendSafe();
|
||||
} catch (err) {
|
||||
// 截断失败时保持原文件继续追加,避免丢日志。
|
||||
}
|
||||
}
|
||||
|
||||
private levelTag(level: LogLevel): string {
|
||||
switch (level) {
|
||||
case LogLevel.WARN:
|
||||
return 'W';
|
||||
case LogLevel.ERROR:
|
||||
return 'E';
|
||||
case LogLevel.INFO:
|
||||
return 'I';
|
||||
default:
|
||||
return 'D';
|
||||
}
|
||||
}
|
||||
|
||||
private timestamp(): string {
|
||||
const d: Date = new Date();
|
||||
return d.getFullYear() + '-' + this.pad(d.getMonth() + 1) + '-' + this.pad(d.getDate()) +
|
||||
' ' + this.pad(d.getHours()) + ':' + this.pad(d.getMinutes()) + ':' + this.pad(d.getSeconds()) +
|
||||
'.' + this.pad3(d.getMilliseconds());
|
||||
}
|
||||
|
||||
private pad(n: number): string {
|
||||
return n < 10 ? '0' + n : '' + n;
|
||||
}
|
||||
|
||||
private pad3(n: number): string {
|
||||
if (n < 10) {
|
||||
return '00' + n;
|
||||
}
|
||||
if (n < 100) {
|
||||
return '0' + n;
|
||||
}
|
||||
return '' + n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 统一日志出口。
|
||||
*
|
||||
* 业务代码不直接调用 hilog,统一走 Logger,保证:
|
||||
* 1. 域号(domain)唯一且集中分配;
|
||||
* 2. tag 由模块名派生,便于过滤;
|
||||
* 3. 级别可在运行期统一收敛(发布版默认 WARN);
|
||||
* 4. 可选挂接 LogSink(如文件落地),实现与 hilog 同源的诊断日志。
|
||||
*/
|
||||
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
|
||||
/** hilog 域号,取值范围 0x0000–0xFFFF,按业务域集中分配。 */
|
||||
export const LOG_DOMAIN: number = 0xDB01;
|
||||
|
||||
/** 日志级别,数值越大越严重。 */
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3
|
||||
}
|
||||
|
||||
/** 日志落地面(文件、内存缓冲、远程上报等按此接口扩展)。 */
|
||||
export interface LogSink {
|
||||
write(level: LogLevel, tag: string, message: string): void;
|
||||
}
|
||||
|
||||
/** 统一日志器。按模块创建实例后复用,避免重复分配。 */
|
||||
export class Logger {
|
||||
private static minLevel: LogLevel = LogLevel.INFO;
|
||||
private static sink: LogSink | undefined = undefined;
|
||||
private readonly tag: string;
|
||||
|
||||
private constructor(tag: string) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
/** 创建指定模块的日志器。 */
|
||||
static create(tag: string): Logger {
|
||||
return new Logger(tag);
|
||||
}
|
||||
|
||||
/** 设置全局最低输出级别,发布版应设为 WARN 及以上。 */
|
||||
static setMinLevel(level: LogLevel): void {
|
||||
Logger.minLevel = level;
|
||||
}
|
||||
|
||||
static getMinLevel(): LogLevel {
|
||||
return Logger.minLevel;
|
||||
}
|
||||
|
||||
/** 挂接落地面;传 undefined 表示取消。 */
|
||||
static setSink(sink: LogSink | undefined): void {
|
||||
Logger.sink = sink;
|
||||
}
|
||||
|
||||
/** 用 {0}、{1} 占位符拼接消息,避免业务侧自行拼字符串。 */
|
||||
private format(message: string, args: string[]): string {
|
||||
let text: string = message;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
text = text.replace('{' + i + '}', args[i]);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private emit(level: LogLevel, message: string, args: string[]): void {
|
||||
if (level < Logger.minLevel) {
|
||||
return;
|
||||
}
|
||||
const text: string = this.format(message, args);
|
||||
switch (level) {
|
||||
case LogLevel.DEBUG:
|
||||
hilog.debug(LOG_DOMAIN, this.tag, '%{public}s', text);
|
||||
break;
|
||||
case LogLevel.INFO:
|
||||
hilog.info(LOG_DOMAIN, this.tag, '%{public}s', text);
|
||||
break;
|
||||
case LogLevel.WARN:
|
||||
hilog.warn(LOG_DOMAIN, this.tag, '%{public}s', text);
|
||||
break;
|
||||
case LogLevel.ERROR:
|
||||
default:
|
||||
hilog.error(LOG_DOMAIN, this.tag, '%{public}s', text);
|
||||
break;
|
||||
}
|
||||
const sink = Logger.sink;
|
||||
if (sink !== undefined) {
|
||||
sink.write(level, this.tag, text);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, ...args: string[]): void {
|
||||
this.emit(LogLevel.DEBUG, message, args);
|
||||
}
|
||||
|
||||
info(message: string, ...args: string[]): void {
|
||||
this.emit(LogLevel.INFO, message, args);
|
||||
}
|
||||
|
||||
warn(message: string, ...args: string[]): void {
|
||||
this.emit(LogLevel.WARN, message, args);
|
||||
}
|
||||
|
||||
error(message: string, ...args: string[]): void {
|
||||
this.emit(LogLevel.ERROR, message, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 凭据保险箱(1.3.8)。
|
||||
*
|
||||
* 结构:
|
||||
* CredentialVault —— 编排层:确保密钥存在、加密后写入 KeyValueStore、读取时解密;
|
||||
* KeyProvider —— 密钥与加解密能力抽象,HUKS 实现在平台适配器中提供;
|
||||
* InMemoryKeyProvider —— 测试/降级实现(仅做可逆标记,不具备保密性,禁止用于生产)。
|
||||
*
|
||||
* 约束(对应 SEC-001 设计):
|
||||
* - 明文只在 put/get 的调用栈中存在,不写入日志、不进入界面状态;
|
||||
* - 密钥别名按保险箱实例隔离;
|
||||
* - 加密失败不得降级为明文。
|
||||
*/
|
||||
import { KeyValueStore } from '../storage/KeyValueStore';
|
||||
|
||||
/** 凭据条目前缀,便于按域清理。 */
|
||||
const CRED_PREFIX: string = 'cred.';
|
||||
|
||||
/** 密钥提供者:生成/存在性/删除/加解密。 */
|
||||
export interface KeyProvider {
|
||||
generate(keyAlias: string): Promise<void>;
|
||||
has(keyAlias: string): Promise<boolean>;
|
||||
delete(keyAlias: string): Promise<void>;
|
||||
encrypt(keyAlias: string, plain: string): Promise<string>;
|
||||
decrypt(keyAlias: string, cipher: string): Promise<string>;
|
||||
}
|
||||
|
||||
/** 凭据存储契约。 */
|
||||
export interface CredentialStore {
|
||||
put(ref: string, secret: string): Promise<void>;
|
||||
get(ref: string): Promise<string | undefined>;
|
||||
has(ref: string): Promise<boolean>;
|
||||
remove(ref: string): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
/** 测试用密钥提供者:可逆标记,不具备安全性。 */
|
||||
export class InMemoryKeyProvider implements KeyProvider {
|
||||
private readonly keys: Set<string> = new Set<string>();
|
||||
|
||||
async generate(keyAlias: string): Promise<void> {
|
||||
this.keys.add(keyAlias);
|
||||
}
|
||||
|
||||
async has(keyAlias: string): Promise<boolean> {
|
||||
return this.keys.has(keyAlias);
|
||||
}
|
||||
|
||||
async delete(keyAlias: string): Promise<void> {
|
||||
this.keys.delete(keyAlias);
|
||||
}
|
||||
|
||||
async encrypt(keyAlias: string, plain: string): Promise<string> {
|
||||
if (!this.keys.has(keyAlias)) {
|
||||
throw new Error('key not found: ' + keyAlias);
|
||||
}
|
||||
return 'enc:' + plain;
|
||||
}
|
||||
|
||||
async decrypt(keyAlias: string, cipher: string): Promise<string> {
|
||||
if (!this.keys.has(keyAlias)) {
|
||||
throw new Error('key not found: ' + keyAlias);
|
||||
}
|
||||
if (cipher.indexOf('enc:') !== 0) {
|
||||
throw new Error('invalid cipher text');
|
||||
}
|
||||
return cipher.substring(4);
|
||||
}
|
||||
}
|
||||
|
||||
export class CredentialVault implements CredentialStore {
|
||||
private readonly store: KeyValueStore;
|
||||
private readonly provider: KeyProvider;
|
||||
private readonly keyAlias: string;
|
||||
|
||||
constructor(store: KeyValueStore, provider: KeyProvider, keyAlias: string) {
|
||||
if (keyAlias.length === 0) {
|
||||
throw new Error('keyAlias must not be empty');
|
||||
}
|
||||
this.store = store;
|
||||
this.provider = provider;
|
||||
this.keyAlias = keyAlias;
|
||||
}
|
||||
|
||||
async put(ref: string, secret: string): Promise<void> {
|
||||
if (ref.length === 0) {
|
||||
throw new Error('credential ref must not be empty');
|
||||
}
|
||||
if (secret.length === 0) {
|
||||
throw new Error('credential secret must not be empty');
|
||||
}
|
||||
if (!(await this.provider.has(this.keyAlias))) {
|
||||
await this.provider.generate(this.keyAlias);
|
||||
}
|
||||
const cipher: string = await this.provider.encrypt(this.keyAlias, secret);
|
||||
await this.store.put(CRED_PREFIX + ref, cipher);
|
||||
}
|
||||
|
||||
async get(ref: string): Promise<string | undefined> {
|
||||
const cipher: string | undefined = await this.store.get(CRED_PREFIX + ref);
|
||||
if (cipher === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return this.provider.decrypt(this.keyAlias, cipher);
|
||||
}
|
||||
|
||||
async has(ref: string): Promise<boolean> {
|
||||
return (await this.store.get(CRED_PREFIX + ref)) !== undefined;
|
||||
}
|
||||
|
||||
async remove(ref: string): Promise<void> {
|
||||
await this.store.delete(CRED_PREFIX + ref);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
const keys: string[] = await this.store.keys();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (keys[i].indexOf(CRED_PREFIX) === 0) {
|
||||
await this.store.delete(keys[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 键值存储抽象(1.3.7)。
|
||||
*
|
||||
* 平台实现(Preferences / RDB / 文件)在后续适配器中提供;本文件只定义契约与
|
||||
* 内存实现,便于在无平台环境测试业务逻辑,也便于替换存储介质。
|
||||
*
|
||||
* 约定:键使用命名空间前缀(如 conn./meta./ui.),便于按域清理与迁移。
|
||||
*/
|
||||
export interface KeyValueStore {
|
||||
get(key: string): Promise<string | undefined>;
|
||||
put(key: string, value: string): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
keys(): Promise<string[]>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
/** 内存实现:仅用于测试与降级(不可持久化)。 */
|
||||
export class MemoryKeyValueStore implements KeyValueStore {
|
||||
private readonly data: Map<string, string> = new Map<string, string>();
|
||||
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
async put(key: string, value: string): Promise<void> {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
this.data.delete(key);
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
this.data.forEach((_value: string, key: string) => {
|
||||
out.push(key);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* LRU 缓存(1.3.7)。
|
||||
*
|
||||
* 用途:对象元数据、查询结果分页、连接信息等热点数据的本地淘汰缓存。
|
||||
* 纯逻辑实现,不依赖平台 API;上层可用 Preferences / RDB / 文件做持久化适配。
|
||||
*
|
||||
* 语义:get 命中会把条目移到最近使用位置;put 超出容量时淘汰最久未使用项。
|
||||
*/
|
||||
export class LruCache<K, V> {
|
||||
private readonly capacity: number;
|
||||
private readonly map: Map<K, V> = new Map<K, V>();
|
||||
private order: K[] = [];
|
||||
|
||||
constructor(capacity: number) {
|
||||
if (capacity < 1) {
|
||||
throw new Error('LruCache capacity must be >= 1');
|
||||
}
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
if (!this.map.has(key)) {
|
||||
return undefined;
|
||||
}
|
||||
this.touch(key);
|
||||
return this.map.get(key);
|
||||
}
|
||||
|
||||
put(key: K, value: V): void {
|
||||
if (this.map.has(key)) {
|
||||
this.map.set(key, value);
|
||||
this.touch(key);
|
||||
return;
|
||||
}
|
||||
this.map.set(key, value);
|
||||
this.order.push(key);
|
||||
while (this.order.length > this.capacity) {
|
||||
const oldest: K | undefined = this.order.shift();
|
||||
if (oldest !== undefined) {
|
||||
this.map.delete(oldest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.map.has(key);
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
if (!this.map.has(key)) {
|
||||
return false;
|
||||
}
|
||||
this.map.delete(key);
|
||||
const idx: number = this.order.indexOf(key);
|
||||
if (idx >= 0) {
|
||||
this.order.splice(idx, 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.order.length;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.map.clear();
|
||||
this.order = [];
|
||||
}
|
||||
|
||||
/** 按最近使用顺序返回键(最旧在前),用于诊断。 */
|
||||
keys(): K[] {
|
||||
const out: K[] = [];
|
||||
for (let i = 0; i < this.order.length; i++) {
|
||||
out.push(this.order[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private touch(key: K): void {
|
||||
const idx: number = this.order.indexOf(key);
|
||||
if (idx >= 0) {
|
||||
this.order.splice(idx, 1);
|
||||
}
|
||||
this.order.push(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 任务调度框架(1.3.5)。
|
||||
*
|
||||
* 目标:把「并发上限、排队、取消、失败传播」收敛到一处,业务侧只提交任务。
|
||||
* 执行体抽象为 TaskRunner,UI 层与 Worker/TaskPool 的差异由实现方注入,
|
||||
* 便于在无平台环境做单元测试,也便于后续替换为 TaskPool 的真实执行器。
|
||||
*
|
||||
* 语义:
|
||||
* - submit 返回的 Promise 在任务成功时 resolve,失败或取消时 reject;
|
||||
* - 取消仅在任务尚未开始(排队中)时生效,返回 true;运行中的任务需自行响应取消信号;
|
||||
* - 并发达到上限时排队,任务完成后自动补位。
|
||||
*/
|
||||
|
||||
/** 可执行任务。 */
|
||||
export interface TaskRunner {
|
||||
run(): Promise<void>;
|
||||
}
|
||||
|
||||
/** 取消错误。 */
|
||||
export class TaskCancelledError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'TaskCancelledError';
|
||||
}
|
||||
}
|
||||
|
||||
/** 排队中的任务项。 */
|
||||
class QueuedTask {
|
||||
readonly id: string;
|
||||
readonly runner: TaskRunner;
|
||||
readonly resolve: (value: void) => void;
|
||||
readonly reject: (reason: Error) => void;
|
||||
|
||||
constructor(id: string, runner: TaskRunner, resolve: (value: void) => void, reject: (reason: Error) => void) {
|
||||
this.id = id;
|
||||
this.runner = runner;
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
}
|
||||
}
|
||||
|
||||
export class TaskScheduler {
|
||||
private readonly maxConcurrency: number;
|
||||
private readonly queue: QueuedTask[] = [];
|
||||
private readonly runningIds: Set<string> = new Set<string>();
|
||||
private runningCount: number = 0;
|
||||
|
||||
constructor(maxConcurrency: number) {
|
||||
if (maxConcurrency < 1) {
|
||||
throw new Error('maxConcurrency must be >= 1');
|
||||
}
|
||||
this.maxConcurrency = maxConcurrency;
|
||||
}
|
||||
|
||||
/** 提交任务;返回完成 Promise。 */
|
||||
submit(id: string, runner: TaskRunner): Promise<void> {
|
||||
if (this.runningIds.has(id)) {
|
||||
return Promise.reject(new Error('duplicate running task id: ' + id));
|
||||
}
|
||||
for (let i = 0; i < this.queue.length; i++) {
|
||||
if (this.queue[i].id === id) {
|
||||
return Promise.reject(new Error('duplicate queued task id: ' + id));
|
||||
}
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.queue.push(new QueuedTask(id, runner, resolve, reject));
|
||||
this.pump();
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消排队中的任务;返回是否取消成功。 */
|
||||
cancel(id: string): boolean {
|
||||
for (let i = 0; i < this.queue.length; i++) {
|
||||
if (this.queue[i].id === id) {
|
||||
const task: QueuedTask = this.queue[i];
|
||||
this.queue.splice(i, 1);
|
||||
task.reject(new TaskCancelledError('task cancelled: ' + id));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingCount(): number {
|
||||
return this.queue.length;
|
||||
}
|
||||
|
||||
activeCount(): number {
|
||||
return this.runningCount;
|
||||
}
|
||||
|
||||
private pump(): void {
|
||||
while (this.runningCount < this.maxConcurrency && this.queue.length > 0) {
|
||||
const task: QueuedTask = this.queue.shift() as QueuedTask;
|
||||
this.runningCount++;
|
||||
this.runningIds.add(task.id);
|
||||
task.runner.run()
|
||||
.then(() => {
|
||||
this.finish(task.id);
|
||||
task.resolve();
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.finish(task.id);
|
||||
task.reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private finish(id: string): void {
|
||||
this.runningIds.delete(id);
|
||||
this.runningCount--;
|
||||
this.pump();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "core",
|
||||
"type": "har",
|
||||
"deviceTypes": [
|
||||
"tablet",
|
||||
"2in1"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "core_module_desc",
|
||||
"value": "DBMGR 核心能力模块(日志、应用信息、公共类型)"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user