wenmo8 发布的文章

前言

RTC

RTC 即实时时钟(Real-Time Clock),主要是功能有:

  1. 时间保持:RTC可以在断电的时候,仍然保持计时功能,保证时间的连续性
  2. 时间显示与设置:RTC可以向系统提供年、月、日、时、分、秒等信息,系统也可以通过接口校准RTC的时间保证准确性

关机闹钟PowerOffAlarm

PowerOffAlarm 是一个与安卓系统关机闹钟功能相关的应用或组件。
当用户设置好关机闹钟后,会向 PowerOffAlarm 发送设定关机闹钟广播并传入闹钟时间参数,PowerOffAlarm 接收到广播后,根据预设提前开机时间和闹钟时间往实时时钟(RTC)中写入时间,并将该时间写入文件中暂存
需要注意的是,PowerOffAlarm 中使用的时间默认是当前时区的时间,若传入的时间戳是其他时区的,则需要调整为当前时区的时间戳

时区

时区是为了适应地球自转造成的不同地区时间差异而划分的区域。

正文

源码分析

关机闹钟 PowerOffAlarm 源码路径:
vendor/qcom/proprietary/PowerOffAlarm/src/com/qualcomm/qti/poweroffalarm

首先查看 AndroidManifest.xml 中与关机闹钟相关的广播接收器的源代码:

<receiver android:name=".PowerOffAlarmBroadcastReceiver"
    android:permission="org.codeaurora.permission.POWER_OFF_ALARM"
    android:exported="true"
    android:directBootAware="true"
    android:label="PowerOffAlarmBroadcastReceiver">
    <intent-filter android:priority="1000">
        <!-- 取消闹钟的广播 -->
        <action android:name="org.codeaurora.poweroffalarm.action.CANCEL_ALARM" />
        <!-- 设置闹钟的广播 -->
        <action android:name="org.codeaurora.poweroffalarm.action.SET_ALARM" />
        <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
    </intent-filter>
</receiver>
<receiver
    android:name="com.qualcomm.qti.poweroffalarm.PowerOffAlarmDialog$ShutDownReceiver"
    android:permission="org.codeaurora.permission.POWER_OFF_ALARM">
    <intent-filter>
        <!-- 关机广播 -->
        <action android:name="org.codeaurora.poweroffalarm.action.ALARM_POWER_OFF"/>
    </intent-filter>
</receiver>

对于设置关机闹钟和取消关机闹钟的逻辑,跟踪到 PowerOffAlarmBroadcastReceiver 的源代码:
\vendor\qcom\proprietary\PowerOffAlarm\src\com\qualcomm\qti\poweroffalarm\PowerOffAlarmBroadcastReceiver.java

/*******************************************************************************
@file    PowerOffAlarmBroadcastReceiver.java
@brief   Receive "org.codeaurora.poweroffalarm.action.SET_ALARM" action to set
         power off alarm and receive "org.codeaurora.poweroffalarm.action.CANCEL_ALARM"
         action to cancel alarm.
******************************************************************************/

public class PowerOffAlarmBroadcastReceiver extends BroadcastReceiver {
    //设置关机闹钟的动作
    private static final String ACTION_SET_POWEROFF_ALARM = "org.codeaurora.poweroffalarm.action.SET_ALARM";
    //设置取消关机闹钟的动作
    private static final String ACTION_CANCEL_POWEROFF_ALARM = "org.codeaurora.poweroffalarm.action.CANCEL_ALARM";
    //设置关机闹钟的意图携带的 extra 的 key
    private static final String TIME = "time";
    //设置或取消闹钟
    if (ACTION_SET_POWEROFF_ALARM.equals(action)) {//设置闹钟动作
        long alarmTime = intent.getLongExtra(TIME, PowerOffAlarmUtils.DEFAULT_ALARM_TIME);//取出要设置的时间戳
        long alarmInPref = PowerOffAlarmUtils.getAlarmFromPreference(context);//获取之前设置的时间戳
        Log.d(TAG, "Set power off alarm : alarm time " + alarmTime + " time in pref " + alarmInPref);
        PowerOffAlarmUtils.saveAlarmToPreference(context, alarmTime);//保存新的时间戳
        long alarmTimeToRtc = PowerOffAlarmUtils.setAlarmToRtc(alarmTime);//转化为 RTC 的时间
        if (alarmTimeToRtc != FAILURE) {//是个合法时间
            persistData.setAlarmTime(alarmTime);
            persistData.setAlarmStatus(PowerOffAlarmUtils.ALARM_STATUS_TO_FIRE);
            persistData.setSnoozeTime(PowerOffAlarmUtils.DEFAULT_ALARM_TIME);
            persistData.writeDataToFile();//会把设置的 RTC 时间存在文件里面
            PowerOffAlarmUtils.saveRtcAlarmToPreference(context, alarmTimeToRtc);
        }
    } else if (ACTION_CANCEL_POWEROFF_ALARM.equals(action)){//取消闹钟动作
        long alarmTime = intent.getLongExtra(TIME, PowerOffAlarmUtils.DEFAULT_ALARM_TIME);//获取要取消的闹钟时间戳
        long alarmInPref = PowerOffAlarmUtils.getAlarmFromPreference(context);//获取之前设置的时间戳
        if (alarmTime == alarmInPref) {//与之前设置的时间戳相同,那就可以取消设置的闹钟
            PowerOffAlarmUtils.saveAlarmToPreference(context, PowerOffAlarmUtils.DEFAULT_ALARM_TIME);
            PowerOffAlarmUtils.saveRtcAlarmToPreference(context, PowerOffAlarmUtils.DEFAULT_ALARM_TIME);
            int rtc = PowerOffAlarmUtils.cancelAlarmInRtc();
            if (rtc < 0) {
                Log.d(TAG, "Cancel alarm time in rtc failed ");
            }
            persistData.setAlarmStatus(PowerOffAlarmUtils.ALARM_STATUS_NONE);
            persistData.setSnoozeTime(PowerOffAlarmUtils.DEFAULT_ALARM_TIME);
            persistData.setAlarmTime(PowerOffAlarmUtils.DEFAULT_ALARM_TIME);
            persistData.writeDataToFile();
        }
    }
}

最后追踪到 PowerOffAlarmUtils 的源代码:
\vendor\qcom\proprietary\PowerOffAlarm\src\com\qualcomm\qti\poweroffalarm\PowerOffAlarmUtils.java

/**
 * Set alarm time to rtc register
 *
 * @param time alarm time based on current time (ms)
 * @return set result -- Fail, return FAILURE; Success,
 *         return the alarm time to rtc
 */
public static final long MS_IN_ONE_MIN = 60000L;//一分钟
private static final long SEC_TO_MS = 1000L;//将秒转化为毫秒

public static long setAlarmToRtc(long alarmTime/*未来时间的时间戳*/) {
    long currentTime = System.currentTimeMillis();//当前时间的时间戳
    long alarmInRtc = getAlarmFromRtc();
    long rtcTime = getRtcTime();
    // MS_IN_ONE_MIN 是系统预留的开机时间
    long timeDelta = alarmTime - currentTime - MS_IN_ONE_MIN;//获取当前到未来的时间戳的差值(单位:ms)
    // alarm in two minute is not supported
    if (timeDelta <= 0) {
        Log.d(TAG, "setAlarmToRtc failed: alarm time is in one minute");
        return FAILURE;//设置的时间若过短就返回失败
    }
    long alarmTimeToRtc = timeDelta / SEC_TO_MS + rtcTime;//计算出要唤醒机器的 RTC 时间(单位:ms)
    try {
        IAlarm mProxy = IAlarm.getService();
        int ret = mProxy.setAlarm(alarmTimeToRtc);//设置 RTC 时间
        if (ret == SUCCESS) {//设置成功
            return alarmTimeToRtc;
        } else {
            return FAILURE;//设置失败
        }
    } catch (Exception e) {
        Log.d(TAG, e.toString());
        return FAILURE;
    }
}

对于关机广播的源代码,首先追踪到源代码:
\vendor\qcom\proprietary\PowerOffAlarm\src\com\qualcomm\qti\poweroffalarm.java

public class PowerOffAlarmDialog  extends Activity{
    public static class ShutDownReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(final Context context, Intent intent) {
            PowerOffAlarmUtils.powerOff(context);//关机
        }
    }
}

随后追踪到 PowerOffAlarmUtils.java 中:

private static final String ACTION_REQUEST_SHUTDOWN = "com.android.internal.intent.action.REQUEST_SHUTDOWN";

public static void powerOff(Context context) {
   //想要直接使用这个函数的话,app必须是系统app,且具有如下权限:
   //<uses-permission android:name="android.permission.SHUTDOWN"/>
   Intent requestShutdown = new Intent(ACTION_REQUEST_SHUTDOWN);
   requestShutdown.putExtra(EXTRA_KEY_CONFIRM, false);//true 是弹窗询问,false 是不弹窗直接关机
   requestShutdown.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   context.startActivity(requestShutdown);
}

项目实战

设置关机闹钟

public static final String POWER_OFF_ALARM_PACKAGE = "com.qualcomm.qti.poweroffalarm";
private static final long MS_IN_ONE_MIN = 6000L;

/**
* 设置关机闹钟,在到达预定的时间戳时开机
*
* @param mContext 上下文
* @param millis 未来的时间戳
*/
public static void startPowerOnAlarm(Context mContext, long millis) {//开机
    Intent intent = new Intent(Constants.ACTION_SET_POWEROFF_ALARM);
    intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    intent.setPackage(POWER_OFF_ALARM_PACKAGE);
    long time = millis + MS_IN_ONE_MIN;
    intent.putExtra("time", time);
    mContext.sendBroadcast(intent);
}

取消关机闹钟

public static final String ACTION_CANCEL_POWEROFF_ALARM = "org.codeaurora.poweroffalarm.action.CANCEL_ALARM";

/** 
* 取消关机闹钟,取消设定在时间戳 millis 的闹钟
* 
* @param mContext 上下文
* @param millis 取消的闹钟时间戳
*/
public static void cancelAlarm(Context mContext, long millis) {
    Intent intent = new Intent(ACTION_CANCEL_POWEROFF_ALARM);
    intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    intent.setPackage(POWER_OFF_ALARM_PACKAGE);
    millis += MS_IN_ONE_MIN;
    intent.putExtra("time", millis);
    sendBroadcast(intent);
}

关机功能

public static void startShutDown(Context mContext) {//关机
    Intent intent = new Intent("org.codeaurora.poweroffalarm.action.ALARM_POWER_OFF");
    intent.setPackage(POWER_OFF_ALARM_PACKAGE);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    mContext.sendBroadcast(intent);
    //也可以使用如下代码实现,但必须是系统应用且带有如下权限:
    // <uses-permission android:name="android.permission.SHUTDOWN"/>
//        String action = "com.android.internal.intent.action.REQUEST_SHUTDOWN";
//        if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.N){
//            action = "android.intent.action.ACTION_REQUEST_SHUTDOWN";
//        }
//        Intent intent = new Intent(action);
//        intent.putExtra("android.intent.extra.KEY_CONFIRM", false);
//        intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
//        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//        mContext.startActivity(intent);
}

本文是高级前端加解密与验签实战的第2篇文章,本系列文章实验靶场为Yakit里自带的Vulinbox靶场,本文讲述的是绕过SHA256+RSA签名来爆破登录。

绕过

根据提示可以看出这次签名用了SHA2556和RSA两个技术进行加密。

查看源代码可以看到RSA公钥是通过请求服务器获取:

请求一下:
http://127.0.0.1:8787/crypto/js/rsa/public/key
,可以看到公钥。

SHA256密钥位置:

Encrypt
方法:

function Encrypt(word) {
    console.info(word);
    return  KEYUTIL.getKey(pubkey).encrypt(CryptoJS.HmacSHA256(word, key.toString(CryptoJS.enc.Utf8)).toString()); 
}

KEYUTIL.getKey(pubkey).encrypt
是RSA1v15加密方法,在代码中可以看到先进行SHA265加密,然后再RSA加密。被加密的文本的格式同
上文
所示。

使用CyberChef加密:

替换请求,可以看到签名构造成功:

热加载

这是我写的Yakit热加载代码,通过
beforeRequest
劫持请求包,使用
encryptData
函数进行加密,
getPubkey
获取公钥,最终实现热加载自动签名功能。

getPubkey = func() {
    //通过请求动态获取公钥
    rsp, req = poc.HTTP(`GET /crypto/js/rsa/public/key HTTP/1.1
Host: 127.0.0.1:8787

    `)~
    body = poc.GetHTTPPacketBody(rsp) // 响应体
    return body
}

encryptData = (packet) => {
    body = poc.GetHTTPPacketBody(packet)
    params = json.loads(body)
    name = params.username
    pass = params.password
    key = "31323334313233343132333431323334"
    pemBytes = getPubkey() // 获取公钥

    signText = f`username=${name}&password=${pass}`
    sha256sign = codec.EncodeToHex(codec.HmacSha256(f`${codec.DecodeHex(key)~}`, signText)) // SHA256加密
    rsaSign = codec.EncodeToHex(codec.RSAEncryptWithPKCS1v15(pemBytes /*type: []byte*/, sha256sign)~) // RSA加密

    body = f`{"username":"${name}","password":"${pass}","signature":"${rsaSign}","key":"${key}"}`
    return string(poc.ReplaceBody(packet, body, false))
}


//发送到服务端修改数据包
// beforeRequest = func(req){
//     return encryptData(req)
// }

//调试用
packet = <<<TEXT
POST /crypto/sign/hmac/sha256/verify HTTP/1.1
Host: 127.0.0.1:8787
Content-Type: application/json
Content-Length: 179

{"username":"admin","password":"password"}
TEXT
result = (encryptData(packet))
print(result)

这次不调试了,直接请求看看效果,成功热加载自动签名:

插入临时字典爆破,可以看到正确密码为admin123。

一、引言

在软件开发过程中,接口测试是确保软件各个组件之间数据传输和功能交互正常工作的重要环节。通过接口测试,可以提高软件的整体质量和稳定性。Pytest是一个流行的Python自动化测试框架,提供了丰富的断言方法和灵活的测试组织结构,使得编写接口测试用例变得简单而高效。本文将详细介绍如何使用Pytest框架进行接口自动化测试,包括开发思想、开发流程以及详细的代码示例。

二、Pytest简介

Pytest是一个强大的Python自动化测试框架,它提供了丰富的功能来帮助用户编写、运行和组织测试用例。Pytest是一个成熟且灵活的Python单元测试框架,相比unittest更加简洁易用。Pytest支持丰富的插件,能够扩展其功能,例如生成HTML测试报告、多线程运行测试用例、控制用例执行顺序等。此外,Pytest还支持与Selenium、Requests等库结合,实现Web自动化、接口自动化和App自动化测试。

三、开发思想

接口自动化测试框架的核心思想是通过封装一些自动化的基础模块、管理模块和统计模块,来提高测试效率,降低维护成本,减少人工干预,增加代码的重用性。以下是开发接口自动化测试框架的主要步骤:

  1. 需求分析
    :明确测试的目标和范围,确定需要测试的接口及其功能。
  2. 环境准备
    :配置Python环境,安装Pytest和Requests库。
  3. 设计测试用例
    :根据接口文档,设计测试用例,包括请求参数、预期结果等。
  4. 编写测试代码
    :使用Pytest框架编写测试代码,包括发送HTTP请求、断言结果等。
  5. 执行测试
    :运行测试代码,收集测试结果。
  6. 生成报告
    :根据测试结果生成测试报告,分析测试结果。

四、开发流程

  1. 环境准备

    首先,需要安装Python和Pytest。可以通过以下命令进行安装:

    pip install python
    pip install pytest
    pip install requests
    
  2. 设计测试用例

    假设我们有一个名为
    example.com
    的网站,该网站提供了一个RESTful API,用于获取用户列表。我们需要编写测试用例来测试这个API。

  3. 编写测试代码

    创建一个名为
    test_api.py
    的文件,并添加以下代码:

    import pytest
    import requests
     
    # 定义测试函数
    def test_get_user_list():
        # 请求URL
        url = "http://example.com/api/v1/users"
        
        # 发送GET请求
        response = requests.get(url)
        
        # 断言状态码为200
        assert response.status_code == 200
        
        # 断言返回的内容包含"users"字段
        assert "users" in response.json()
        
        # 断言返回的用户列表不为空
        assert len(response.json()["users"]) > 0
     
    if __name__ == "__main__":
        pytest.main()
    
  4. 执行测试

    使用以下命令运行测试代码:

    bash复制代码
    
    pytest test_api.py
    

    运行后,Pytest将自动发现并执行测试函数,并输出测试结果。

  5. 生成报告

    Pytest支持生成HTML格式的测试报告。为了生成HTML报告,需要安装
    pytest-html
    插件:

    bash复制代码
    
    pip install pytest-html
    

    然后,使用以下命令运行测试并生成HTML报告:

    bash复制代码
    
    pytest --html=report.html test_api.py
    

    运行后,将在当前目录下生成一个名为
    report.html
    的测试报告文件,可以通过浏览器打开查看测试结果。

五、高级功能

  1. 参数化测试

    参数化测试允许为同一个测试用例提供多个输入参数,并让Pytest自动为每个参数运行一次测试。这可以大大提高测试的覆盖率,同时减少重复的测试代码。

    以下是一个参数化测试的示例:

    import pytest
    import requests
     
    # 参数化测试数据
    test_data = [
        ("http://example.com/api/v1/users", 200),
        ("http://example.com/api/v1/nonexistent", 404),
    ]
     
    # 参数化测试函数
    @pytest.mark.parametrize("url, expected_status", test_data)
    def test_different_urls(url, expected_status):
        response = requests.get(url)
        assert response.status_code == expected_status
    
  2. 前后置处理

    在测试用例执行之前和之后,可能需要进行一些准备工作和清理工作。Pytest提供了
    setup_function

    teardown_function
    等函数来实现前后置处理。

    以下是一个前后置处理的示例:

    import pytest
    import requests
     
    # 前置处理函数
    def setup_function():
        print("前置处理:设置测试环境")
     
    # 后置处理函数
    def teardown_function():
        print("后置处理:清理测试环境")
     
    # 测试函数
    def test_get_user_list():
        url = "http://example.com/api/v1/users"
        response = requests.get(url)
        assert response.status_code == 200
        assert "users" in response.json()
        assert len(response.json()["users"]) > 0
     
    # 注册前后置处理函数
    pytest.hookimpl(tryfirst=True, hookwrapper=True)(setup_function)
    pytest.hookimpl(trylast=True, hookwrapper=True)(teardown_function)
    

    注意:在实际应用中,建议使用
    pytest

    fixture
    机制来实现前后置处理,因为它更加灵活和强大。

  3. 失败重试

    对于可能因网络波动等原因导致偶尔失败的测试用例,可以使用
    pytest-rerunfailures
    插件来实现失败重试。

    首先,安装
    pytest-rerunfailures
    插件:

    bash复制代码
    
    pip install pytest-rerunfailures
    

    然后,在测试函数上使用
    @pytest.mark.flaky(reruns=N, reruns_delay=T)
    装饰器来指定重试次数和重试间隔。

    以下是一个失败重试的示例:

    import pytest
    import requests
     
    @pytest.mark.flaky(reruns=3, reruns_delay=2)
    def test_get_user_list():
        url = "http://example.com/api/v1/users"
        response = requests.get(url)
        assert response.status_code == 200
        assert "users" in response.json()
        assert len(response.json()["users"]) > 0
    

六、完整代码示例

以下是一个完整的接口自动化测试框架的代码示例,包括环境准备、测试用例设计、测试代码编写、执行测试和生成报告等步骤:

# test_api.py
 
import pytest
import requests
 
# 前置处理函数
@pytest.fixture(scope="module")
def api_client():
    print("前置处理:设置测试环境")
    base_url = "http://example.com/api/v1"
    return lambda path, **kwargs: requests.get(f"{base_url}/{path}", **kwargs)
 
# 参数化测试数据
test_data = [
    ("/users", 200),
    ("/nonexistent", 404),
]
 
# 参数化测试函数
@pytest.mark.parametrize("path, expected_status", test_data)
def test_different_urls(api_client, path, expected_status):
    response = api_client(path)
    assert response.status_code == expected_status
    if expected_status == 200:
        assert "users" in response.json() if path == "/users" else True
 
# 后置处理函数(通过fixture的scope="module"实现)
def teardown_module(module):
    print("后置处理:清理测试环境")
 
if __name__ == "__main__":
    pytest.main(["--html=report.html"])

运行上述代码后,将在当前目录下生成一个名为
report.html
的测试报告文件,可以通过浏览器打开查看测试结果。

七、总结

本文详细介绍了如何使用Pytest框架进行接口自动化测试,包括开发思想、开发流程以及详细的代码示例。通过Pytest框架,我们可以轻松编写高效、可靠的接口测试用例,提高测试效率和软件质量。希望本文能够对读者在实际开发中有所帮助。

Chats 开发指南

欢迎使用 Chats!在我上一篇博客
https://www.cnblogs.com/sdcb/p/18597030/sdcb-chats-intro
中,我介绍了 Chats 的各种功能,但所有功能都是用代码一行一行写出来的。

在这个指南中,我将帮助您快速上手开发,了解如何在开发阶段使用和配置 Chats 项目。Chats 在开发阶段采用前后端分离的模式,但在生产环境中前后端会合并为一个发布包。

技术基础

  • 后端:
    使用 C#/ASP.NET Core 开发。
  • 前端:
    使用 Next.js/React/TypeScript 开发。
  • CSS:
    使用 Tailwind CSS。

环境需求

  • Git
  • .NET SDK 8.0
  • Node.js >= 20
  • Visual Studio Code
  • Visual Studio 2022(可选但推荐)

获取代码

首先,克隆 Chats 的代码仓库:

git clone https://github.com/sdcb/chats.git

前后端共同开发

后端开发指南

  1. 使用 Visual Studio 打开解决方案:

    在根目录下找到
    chats/Chats.sln
    解决方案文件并打开。在 Visual Studio 中,您将看到一个名为
    Chats.BE
    的网站项目。

  2. 运行项目:


    • 按 F5 运行项目。默认配置会检查 SQLite 数据库文件
      chats.db
      是否存在,如果不存在,会自动创建在
      ./AppData
      目录并初始化数据库。
    • 服务将在
      http://localhost:5146
      上运行,并提供 API 服务。如果在开发模式下运行 (
      ASPNETCORE_ENVIRONMENT=Development
      ),Swagger UI 将在
      http://localhost:5146/swagger
      上可用。
  3. 配置文件说明:

    默认配置在
    appsettings.json
    中,但强烈建议使用
    userSecrets.json
    管理敏感信息。这可以避免在代码库中泄露敏感的开发配置。

    默认配置结构如下:

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*",
      "FE_URL": "http://localhost:3001",
      "ENCRYPTION_PASSWORD": "this is used for encrypt auto increment int id, please set as a random string.",
      "DBType": "sqlite",
      "ConnectionStrings": {
        "ChatsDB": "Data Source=./AppData/chats.db"
      }
    }
    

    配置选项解释:


    • Logging
      : 管理日志级别,默认记录信息级别的日志。
    • AllowedHosts
      : 配置允许访问的主机名,
      *
      表示接受所有。
    • FE_URL
      : 前端的URL,默认指向
      http://localhost:3001
      。前端可以通过 CORS 跨域访问后端。默认3000端口无需额外配置。
    • DBType
      : 数据库类型,支持
      sqlite
      (默认)、
      mssql

      postgresql
    • ConnectionStrings:ChatsDB
      : 数据库
      ADO.NET
      连接字符串,随
      DBType
      而变。
    • ENCRYPTION_PASSWORD
      : 用于加密自增 ID。生产环境中应设置为随机字符串,避免直接暴露 ID。

    为什么使用整数+加密而非 GUID?

    在 Chats 项目初期,我们确实是使用的GUID,但是由于下列2个原因并经过慎重考虑,我换成了自增整数Id:


    • GUID 字段较大,占用更多空间;
    • GUID 作为聚集索引会导致索引碎片,影响性能;

    管理敏感配置:

    不建议在
    appsettings.json
    中直接修改配置项。可以通过 Visual Studio 使用
    userSecrets.json


    • Visual Studio: 右键点击
      Chats.BE
      项目 ->
      管理用户机密

    • CLI: 使用以下命令管理用户机密。

      dotnet user-secrets init
      dotnet user-secrets set "key" "value"
      dotnet user-secrets list
      

    这可以避免在提交代码时不小心将敏感信息上传。

  4. 不使用 Visual Studio 的运行方式:

    进入后端目录:

    cd ./chats/src/BE
    dotnet run
    

前端开发指南

  1. 进入前端目录:

    cd ./chats/src/FE
    
  2. 创建
    .env.local
    文件并指定后端 URL:

    echo "API_URL=http://localhost:5146" > .env.local
    
  3. 安装依赖并运行开发服务器:

    npm i
    npm run dev
    

运行后,前端服务将监听
http://localhost:3000
。后端已有 CORS 配置支持无需额外配置。

仅前端开发

对于专注于前端开发的场景,我们提供了一个已经部署好的后端开发环境:

  1. 克隆仓库:

    git clone https://github.com/sdcb/chats.git
    
  2. 进入前端目录并指定远程后端:

    cd ./chats/src/FE
    echo "API_URL=https://chats-dev.starworks.cc:88" > .env.local
    

    这个环境已经默认允许了
    http://localhost:3000
    这个地址的跨域访问行为。

  3. 安装依赖并运行:

    npm i
    npm run dev
    

注意事项

如果想模拟生产打包过程,请执行:

npm run build

此命令会在当前目录生成
./out
文件夹,其中包含所有必要的静态文件。

仅后端开发

对于专注于后端开发的场景,可以使用打包好的前端文件:

  1. 克隆仓库并进入后端目录:

    git clone https://github.com/sdcb/chats.git
    cd ./chats/src/BE
    
  2. 下载并解压前端静态文件放置到
    wwwroot

    Linux 下执行:

    curl -O https://github.com/sdcb/chats/releases/latest/download/chats-fe.zip
    unzip chats-fe.zip
    cp -r chats-fe/* wwwroot/
    

    Windows 下执行:

    Invoke-WebRequest -Uri "https://github.com/sdcb/chats/releases/latest/download/chats-fe.zip" -OutFile "chats-fe.zip"
    Expand-Archive -Path "chats-fe.zip" -DestinationPath "."
    Copy-Item -Path ".\chats-fe\*" -Destination ".\wwwroot" -Recurse -Force
    

    注意


    1. 我同时还将上述的
      https://github.com/sdcb/chats/releases/latest/download/chats-fe.zip
      地址上传到了我个人的 Minio 文件服务器,地址为:
      http://io.starworks.cc:88/chats/latest/chats-fe.zip

      如果您直接从 Github 上下载速度太慢,可以换成这个地址。

    2. 这个地址对应的
      chats-fe.zip
      附件是由 Github Actions 在代码合入
      main
      分支时自动生成来而,合入
      dev
      分支时并不会触发更新这个文件。

  3. 运行后端:

    dotnet run
    

    或者在 Visual Studio 中打开
    Chats.sln
    并运行
    Chats.BE
    项目。

运行后,访问
http://localhost:5146/login
可以直接进入 Chats 的登录界面,实现前后端不分离的部署模式。

希望此指南可以帮助您顺利开展 Chats 项目的开发工作。如有任何问题,请查看源码中的文档或在
https://github.com/sdcb/chats
中创建issue来获得支持。

人工标注员

这是
人工评估
系列文章的第二篇《人工标注员》,全系列包括:

  • 基础概念
  • 人工标注员
  • 技巧与提示

推荐阅读
这篇综述
的第三章,介绍了许多数据标注质量管理的实践经验。如果你追求的是生产级的质量,并且具备实施条件,那么请继续阅读吧!

Best_annotation_practices

无论项目规模多大,一旦定义了具体的评估任务和打分细则,请注意:

  • 选择合适的标注员,如果可能的话提供经济激励
    你可能希望参与任务的标注员具有以下品质:
  1. 符合特定的人口统计特征。
    例如:母语是测试目标语言、较高的教育水平、特定领域的专业知识、多样化的地域背景等。
    根据评估任务不同,对标注员统计特征需求也不一样。
  2. 提供高质量标注。
    有些任务中筛选合适的标注员很重要,比如近期有一种任务是检查回答是否是 LLM 生成的。
    个人认为,除非你众包标注员有强烈的自我驱动意识,否则一般还是支付合理的费用更好。
  • 设计标注准则
    请务必深入思考制定标注准则,非常值得花费大量时间去做!我们在制作
    GAIA
    数据集时的耗时最多的地方就是这里。

  • 迭代标注
    很多时候标注员会误解标注指南 (他们的想法可能比你想象的更模棱两可),所以要做好多轮迭代标注的准备,来不断改进直到达到你的需求。


    • 质量检查

      手动筛选
      你需要仔细检查答案的质量 (检查标注员间的答案一致性),并筛选出质量最优、相关性最高的答案。

你也可以使用专用工具来构建高质量标注数据集,如
Argilla

深入阅读推荐链接:

原文链接:
https://github.com/huggingface/evaluation-guidebook/blob/main/contents/human-evaluation/using-human-annotators.md

作者: clefourrier

译者: SuSung-boy

审校: adeenayakup